mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-06 09:29:42 +00:00
2f7ec968f4
Look for an entry `PMT_<PID>` in the `prog-map`, which specifies the relative index of the stream in the PMT. Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/1510>
1861 lines
50 KiB
C
1861 lines
50 KiB
C
/*
|
|
* Copyright 2006 BBC and Fluendo S.A.
|
|
*
|
|
* This library is licensed under 3 different licenses and you
|
|
* can choose to use it under the terms of any one of them. The
|
|
* three licenses are the MPL 1.1, the LGPL, and the MIT license.
|
|
*
|
|
* MPL:
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License
|
|
* Version 1.1 (the "License"); you may not use this file except in
|
|
* compliance with the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/.
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS"
|
|
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing rights and limitations
|
|
* under the License.
|
|
*
|
|
* LGPL:
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public
|
|
* License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*
|
|
* MIT:
|
|
*
|
|
* Unless otherwise indicated, Source Code is licensed under MIT license.
|
|
* See further explanation attached in License Statement (distributed in the file
|
|
* LICENSE).
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
* this software and associated documentation files (the "Software"), to deal in
|
|
* the Software without restriction, including without limitation the rights to
|
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
* so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*
|
|
* SPDX-License-Identifier: MPL-1.1 OR MIT OR LGPL-2.0-or-later
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include <string.h>
|
|
|
|
#include <gst/mpegts/mpegts.h>
|
|
|
|
#include "tsmux.h"
|
|
#include "tsmuxstream.h"
|
|
|
|
#define GST_CAT_DEFAULT gst_base_ts_mux_debug
|
|
|
|
/* Maximum total data length for a PAT section is 1024 bytes, minus an
|
|
* 8 byte header, then the length of each program entry is 32 bits,
|
|
* then finally a 32 bit CRC. Thus the maximum number of programs in this mux
|
|
* is (1024 - 8 - 4) / 4 = 253 because it only supports single section PATs */
|
|
#define TSMUX_MAX_PROGRAMS 253
|
|
|
|
#define TSMUX_SECTION_HDR_SIZE 8
|
|
|
|
#define TSMUX_DEFAULT_NETWORK_ID 0x0001
|
|
#define TSMUX_DEFAULT_TS_ID 0x0001
|
|
|
|
/* The last byte of the PCR in the header defines the byte position
|
|
* at which PCR should be calculated */
|
|
#define PCR_BYTE_OFFSET 11
|
|
|
|
/* HACK: We use a fixed buffering offset for the PCR at the moment -
|
|
* this is the amount 'in advance' of the stream that the PCR sits.
|
|
* 1/8 second atm */
|
|
#define TSMUX_PCR_OFFSET (TSMUX_CLOCK_FREQ / 8)
|
|
|
|
/* Base for all written PCR and DTS/PTS,
|
|
* so we have some slack to go backwards */
|
|
#define CLOCK_BASE (TSMUX_CLOCK_FREQ * 10 * 360)
|
|
|
|
static gboolean tsmux_write_pat (TsMux * mux);
|
|
static gboolean tsmux_write_pmt (TsMux * mux, TsMuxProgram * program);
|
|
static gboolean tsmux_write_scte_null (TsMux * mux, TsMuxProgram * program);
|
|
static gint64 get_next_pcr (TsMux * mux, gint64 cur_ts);
|
|
static gint64 get_current_pcr (TsMux * mux, gint64 cur_ts);
|
|
static gint64 write_new_pcr (TsMux * mux, TsMuxStream * stream, gint64 cur_pcr,
|
|
gint64 next_pcr);
|
|
static gboolean tsmux_write_ts_header (TsMux * mux, guint8 * buf,
|
|
TsMuxPacketInfo * pi, guint * payload_len_out, guint * payload_offset_out,
|
|
guint stream_avail);
|
|
|
|
static void
|
|
tsmux_section_free (TsMuxSection * section)
|
|
{
|
|
gst_mpegts_section_unref (section->section);
|
|
g_slice_free (TsMuxSection, section);
|
|
}
|
|
|
|
/**
|
|
* tsmux_new:
|
|
*
|
|
* Create a new muxer session.
|
|
*
|
|
* Returns: A new #TsMux object.
|
|
*/
|
|
TsMux *
|
|
tsmux_new (void)
|
|
{
|
|
TsMux *mux;
|
|
|
|
mux = g_slice_new0 (TsMux);
|
|
|
|
mux->transport_id = TSMUX_DEFAULT_TS_ID;
|
|
|
|
mux->next_pgm_no = TSMUX_START_PROGRAM_ID;
|
|
mux->next_pmt_pid = TSMUX_START_PMT_PID;
|
|
mux->next_stream_pid = TSMUX_START_ES_PID;
|
|
|
|
mux->pat_changed = TRUE;
|
|
mux->next_pat_pcr = -1;
|
|
mux->pat_interval = TSMUX_DEFAULT_PAT_INTERVAL;
|
|
|
|
mux->si_changed = TRUE;
|
|
mux->si_interval = TSMUX_DEFAULT_SI_INTERVAL;
|
|
|
|
mux->pcr_interval = TSMUX_DEFAULT_PCR_INTERVAL;
|
|
|
|
mux->next_si_pcr = -1;
|
|
|
|
mux->si_sections = g_hash_table_new_full (g_direct_hash, g_direct_equal,
|
|
NULL, (GDestroyNotify) tsmux_section_free);
|
|
|
|
mux->new_stream_func = (TsMuxNewStreamFunc) tsmux_stream_new;
|
|
mux->new_stream_data = NULL;
|
|
|
|
mux->first_pcr_ts = G_MININT64;
|
|
|
|
return mux;
|
|
}
|
|
|
|
/**
|
|
* tsmux_set_write_func:
|
|
* @mux: a #TsMux
|
|
* @func: a user callback function
|
|
* @user_data: user data passed to @func
|
|
*
|
|
* Set the callback function and user data to be called when @mux has output to
|
|
* produce. @user_data will be passed as user data in @func.
|
|
*/
|
|
void
|
|
tsmux_set_write_func (TsMux * mux, TsMuxWriteFunc func, void *user_data)
|
|
{
|
|
g_return_if_fail (mux != NULL);
|
|
|
|
mux->write_func = func;
|
|
mux->write_func_data = user_data;
|
|
}
|
|
|
|
/**
|
|
* tsmux_set_alloc_func:
|
|
* @mux: a #TsMux
|
|
* @func: a user callback function
|
|
* @user_data: user data passed to @func
|
|
*
|
|
* Set the callback function and user data to be called when @mux needs
|
|
* a new buffer to write a packet into.
|
|
* @user_data will be passed as user data in @func.
|
|
*/
|
|
void
|
|
tsmux_set_alloc_func (TsMux * mux, TsMuxAllocFunc func, void *user_data)
|
|
{
|
|
g_return_if_fail (mux != NULL);
|
|
|
|
mux->alloc_func = func;
|
|
mux->alloc_func_data = user_data;
|
|
}
|
|
|
|
/**
|
|
* tsmux_set_new_stream_func:
|
|
* @mux: a #TsMux
|
|
* @func: a user callback function
|
|
* @user_data: user data passed to @func
|
|
*
|
|
* Set the callback function and user data to be called when @mux needs
|
|
* to create a new stream.
|
|
* @user_data will be passed as user data in @func.
|
|
*/
|
|
void
|
|
tsmux_set_new_stream_func (TsMux * mux, TsMuxNewStreamFunc func,
|
|
void *user_data)
|
|
{
|
|
g_return_if_fail (mux != NULL);
|
|
|
|
mux->new_stream_func = func;
|
|
mux->new_stream_data = user_data;
|
|
}
|
|
|
|
/**
|
|
* tsmux_set_pat_interval:
|
|
* @mux: a #TsMux
|
|
* @freq: a new PAT interval
|
|
*
|
|
* Set the interval (in cycles of the 90kHz clock) for writing out the PAT table.
|
|
*
|
|
* Many transport stream clients might have problems if the PAT table is not
|
|
* inserted in the stream at regular intervals, especially when initially trying
|
|
* to figure out the contents of the stream.
|
|
*/
|
|
void
|
|
tsmux_set_pat_interval (TsMux * mux, guint freq)
|
|
{
|
|
g_return_if_fail (mux != NULL);
|
|
|
|
mux->pat_interval = freq;
|
|
}
|
|
|
|
/**
|
|
* tsmux_set_pcr_interval:
|
|
* @mux: a #TsMux
|
|
* @freq: a new PCR interval
|
|
*
|
|
* Set the interval (in cycles of the 90kHz clock) for writing the PCR.
|
|
*/
|
|
void
|
|
tsmux_set_pcr_interval (TsMux * mux, guint freq)
|
|
{
|
|
g_return_if_fail (mux != NULL);
|
|
|
|
mux->pcr_interval = freq;
|
|
}
|
|
|
|
/**
|
|
* tsmux_get_pat_interval:
|
|
* @mux: a #TsMux
|
|
*
|
|
* Get the configured PAT interval. See also tsmux_set_pat_interval().
|
|
*
|
|
* Returns: the configured PAT interval
|
|
*/
|
|
guint
|
|
tsmux_get_pat_interval (TsMux * mux)
|
|
{
|
|
g_return_val_if_fail (mux != NULL, 0);
|
|
|
|
return mux->pat_interval;
|
|
}
|
|
|
|
/**
|
|
* tsmux_resend_pat:
|
|
* @mux: a #TsMux
|
|
*
|
|
* Resends the PAT before the next stream packet.
|
|
*/
|
|
void
|
|
tsmux_resend_pat (TsMux * mux)
|
|
{
|
|
g_return_if_fail (mux != NULL);
|
|
|
|
mux->next_pat_pcr = -1;
|
|
}
|
|
|
|
/**
|
|
* tsmux_set_si_interval:
|
|
* @mux: a #TsMux
|
|
* @freq: a new SI table interval
|
|
*
|
|
* Set the interval (in cycles of the 90kHz clock) for writing out the SI tables.
|
|
*
|
|
*/
|
|
void
|
|
tsmux_set_si_interval (TsMux * mux, guint freq)
|
|
{
|
|
g_return_if_fail (mux != NULL);
|
|
|
|
mux->si_interval = freq;
|
|
}
|
|
|
|
/**
|
|
* tsmux_get_si_interval:
|
|
* @mux: a #TsMux
|
|
*
|
|
* Get the configured SI table interval. See also tsmux_set_si_interval().
|
|
*
|
|
* Returns: the configured SI interval
|
|
*/
|
|
guint
|
|
tsmux_get_si_interval (TsMux * mux)
|
|
{
|
|
g_return_val_if_fail (mux != NULL, 0);
|
|
|
|
return mux->si_interval;
|
|
}
|
|
|
|
/**
|
|
* tsmux_resend_si:
|
|
* @mux: a #TsMux
|
|
*
|
|
* Resends the SI tables before the next stream packet.
|
|
*
|
|
*/
|
|
void
|
|
tsmux_resend_si (TsMux * mux)
|
|
{
|
|
g_return_if_fail (mux != NULL);
|
|
|
|
mux->next_si_pcr = -1;
|
|
}
|
|
|
|
/**
|
|
* tsmux_add_mpegts_si_section:
|
|
* @mux: a #TsMux
|
|
* @section: (transfer full): a #GstMpegtsSection to add
|
|
*
|
|
* Add a Service Information #GstMpegtsSection to the stream
|
|
*
|
|
* Returns: %TRUE on success, %FALSE otherwise
|
|
*/
|
|
gboolean
|
|
tsmux_add_mpegts_si_section (TsMux * mux, GstMpegtsSection * section)
|
|
{
|
|
TsMuxSection *tsmux_section;
|
|
|
|
g_return_val_if_fail (mux != NULL, FALSE);
|
|
g_return_val_if_fail (section != NULL, FALSE);
|
|
g_return_val_if_fail (mux->si_sections != NULL, FALSE);
|
|
|
|
tsmux_section = g_slice_new0 (TsMuxSection);
|
|
|
|
GST_DEBUG ("Adding mpegts section with type %d to mux",
|
|
section->section_type);
|
|
|
|
tsmux_section->section = section;
|
|
tsmux_section->pi.pid = section->pid;
|
|
|
|
g_hash_table_insert (mux->si_sections,
|
|
GINT_TO_POINTER (section->section_type), tsmux_section);
|
|
|
|
mux->si_changed = TRUE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
/**
|
|
* tsmux_free:
|
|
* @mux: a #TsMux
|
|
*
|
|
* Free all resources associated with @mux. After calling this function @mux can
|
|
* not be used anymore.
|
|
*/
|
|
void
|
|
tsmux_free (TsMux * mux)
|
|
{
|
|
GList *cur;
|
|
|
|
g_return_if_fail (mux != NULL);
|
|
|
|
/* Free PAT section */
|
|
if (mux->pat.section)
|
|
gst_mpegts_section_unref (mux->pat.section);
|
|
|
|
/* Free all programs */
|
|
for (cur = mux->programs; cur; cur = cur->next) {
|
|
TsMuxProgram *program = (TsMuxProgram *) cur->data;
|
|
|
|
tsmux_program_free (program);
|
|
}
|
|
g_list_free (mux->programs);
|
|
|
|
/* Free all streams */
|
|
for (cur = mux->streams; cur; cur = cur->next) {
|
|
TsMuxStream *stream = (TsMuxStream *) cur->data;
|
|
|
|
tsmux_stream_free (stream);
|
|
}
|
|
g_list_free (mux->streams);
|
|
|
|
/* Free SI table sections */
|
|
g_hash_table_unref (mux->si_sections);
|
|
|
|
g_slice_free (TsMux, mux);
|
|
}
|
|
|
|
static gint
|
|
tsmux_program_compare (TsMuxProgram * program, gint * needle)
|
|
{
|
|
return (program->pgm_number - *needle);
|
|
}
|
|
|
|
/**
|
|
* tsmux_program_new:
|
|
* @mux: a #TsMux
|
|
*
|
|
* Create a new program in the missing session @mux.
|
|
*
|
|
* Returns: a new #TsMuxProgram or %NULL when the maximum number of programs has
|
|
* been reached.
|
|
*/
|
|
TsMuxProgram *
|
|
tsmux_program_new (TsMux * mux, gint prog_id)
|
|
{
|
|
TsMuxProgram *program;
|
|
|
|
g_return_val_if_fail (mux != NULL, NULL);
|
|
|
|
/* Ensure we have room for another program */
|
|
if (mux->nb_programs == TSMUX_MAX_PROGRAMS)
|
|
return NULL;
|
|
|
|
program = g_slice_new0 (TsMuxProgram);
|
|
|
|
program->pmt_changed = TRUE;
|
|
program->pmt_interval = TSMUX_DEFAULT_PMT_INTERVAL;
|
|
|
|
program->next_pmt_pcr = -1;
|
|
|
|
if (prog_id == 0) {
|
|
program->pgm_number = mux->next_pgm_no++;
|
|
while (g_list_find_custom (mux->programs, &program->pgm_number,
|
|
(GCompareFunc) tsmux_program_compare) != NULL) {
|
|
program->pgm_number = mux->next_pgm_no++;
|
|
}
|
|
} else {
|
|
program->pgm_number = prog_id;
|
|
while (g_list_find_custom (mux->programs, &program->pgm_number,
|
|
(GCompareFunc) tsmux_program_compare) != NULL) {
|
|
program->pgm_number++;
|
|
}
|
|
}
|
|
|
|
program->pmt_pid = mux->next_pmt_pid++;
|
|
program->pcr_stream = NULL;
|
|
|
|
/* SCTE35 is disabled by default */
|
|
program->scte35_pid = 0;
|
|
program->scte35_null_interval = TSMUX_DEFAULT_SCTE_35_NULL_INTERVAL;
|
|
program->next_scte35_pcr = -1;
|
|
|
|
/* mux->streams owns the streams */
|
|
program->streams = g_ptr_array_new_full (1, NULL);
|
|
|
|
mux->programs = g_list_prepend (mux->programs, program);
|
|
mux->nb_programs++;
|
|
mux->pat_changed = TRUE;
|
|
|
|
return program;
|
|
}
|
|
|
|
gboolean
|
|
tsmux_program_delete (TsMux * mux, TsMuxProgram * program)
|
|
{
|
|
g_return_val_if_fail (mux != NULL, FALSE);
|
|
|
|
if (mux->nb_programs == 0)
|
|
return FALSE;
|
|
|
|
if (!program)
|
|
return FALSE;
|
|
|
|
mux->programs = g_list_remove (mux->programs, program);
|
|
mux->nb_programs--;
|
|
mux->pat_changed = TRUE;
|
|
tsmux_program_free ((TsMuxProgram *) program);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* tsmux_set_pmt_interval:
|
|
* @program: a #TsMuxProgram
|
|
* @freq: a new PMT interval
|
|
*
|
|
* Set the interval (in cycles of the 90kHz clock) for writing out the PMT table.
|
|
*
|
|
* Many transport stream clients might have problems if the PMT table is not
|
|
* inserted in the stream at regular intervals, especially when initially trying
|
|
* to figure out the contents of the stream.
|
|
*/
|
|
void
|
|
tsmux_set_pmt_interval (TsMuxProgram * program, guint freq)
|
|
{
|
|
g_return_if_fail (program != NULL);
|
|
|
|
program->pmt_interval = freq;
|
|
}
|
|
|
|
/**
|
|
* tsmux_get_pmt_interval:
|
|
* @program: a #TsMuxProgram
|
|
*
|
|
* Get the configured PMT interval. See also tsmux_set_pmt_interval().
|
|
*
|
|
* Returns: the configured PMT interval
|
|
*/
|
|
guint
|
|
tsmux_get_pmt_interval (TsMuxProgram * program)
|
|
{
|
|
g_return_val_if_fail (program != NULL, 0);
|
|
|
|
return program->pmt_interval;
|
|
}
|
|
|
|
/**
|
|
* tsmux_program_set_scte35_interval:
|
|
* @program: a #TsMuxProgram
|
|
* @freq: a new SCTE-35 NULL interval
|
|
*
|
|
* Set the interval (in cycles of the 90kHz clock) for sending out the SCTE-35
|
|
* NULL command. This is only effective is the SCTE-35 PID is not 0.
|
|
*/
|
|
void
|
|
tsmux_program_set_scte35_interval (TsMuxProgram * program, guint interval)
|
|
{
|
|
g_return_if_fail (program != NULL);
|
|
|
|
program->scte35_null_interval = interval;
|
|
}
|
|
|
|
/**
|
|
* tsmux_resend_pmt:
|
|
* @program: a #TsMuxProgram
|
|
*
|
|
* Resends the PMT before the next stream packet.
|
|
*/
|
|
void
|
|
tsmux_resend_pmt (TsMuxProgram * program)
|
|
{
|
|
g_return_if_fail (program != NULL);
|
|
|
|
program->next_pmt_pcr = -1;
|
|
}
|
|
|
|
/**
|
|
* tsmux_program_set_scte35_pid:
|
|
* @program: a #TsMuxProgram
|
|
* @pid: The pid to use, or 0 to deactivate usage.
|
|
*
|
|
* Set the @pid to use for sending SCTE-35 packets on the given
|
|
* @program.
|
|
*
|
|
* This needs to be called as early as possible if SCTE-35 sections
|
|
* are even going to be used with the given @program so that the PMT
|
|
* can be properly configured.
|
|
*/
|
|
void
|
|
tsmux_program_set_scte35_pid (TsMuxProgram * program, guint16 pid)
|
|
{
|
|
TsMuxSection *section;
|
|
GstMpegtsSCTESIT *sit;
|
|
g_return_if_fail (program != NULL);
|
|
|
|
program->scte35_pid = pid;
|
|
/* Create/Update the section */
|
|
if (program->scte35_null_section) {
|
|
tsmux_section_free (program->scte35_null_section);
|
|
program->scte35_null_section = NULL;
|
|
}
|
|
if (pid != 0) {
|
|
program->scte35_null_section = section = g_slice_new0 (TsMuxSection);
|
|
section->pi.pid = pid;
|
|
sit = gst_mpegts_scte_null_new ();
|
|
section->section = gst_mpegts_section_from_scte_sit (sit, pid);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* tsmux_program_get_scte35_pid:
|
|
* @program: a #TsMuxProgram
|
|
*
|
|
* Get the PID configured for sending SCTE-35 packets.
|
|
*
|
|
* Returns: the configured SCTE-35 PID, or 0 if not active.
|
|
*/
|
|
guint16
|
|
tsmux_program_get_scte35_pid (TsMuxProgram * program)
|
|
{
|
|
g_return_val_if_fail (program != NULL, 0);
|
|
|
|
return program->scte35_pid;
|
|
}
|
|
|
|
/**
|
|
* tsmux_program_add_stream:
|
|
* @program: a #TsMuxProgram
|
|
* @stream: a #TsMuxStream
|
|
*
|
|
* Add @stream to @program.
|
|
*/
|
|
void
|
|
tsmux_program_add_stream (TsMuxProgram * program, TsMuxStream * stream)
|
|
{
|
|
GPtrArray *streams;
|
|
guint i;
|
|
gint pmt_index, array_index = -1 /* append */ ;
|
|
guint16 pid;
|
|
|
|
g_return_if_fail (program != NULL);
|
|
g_return_if_fail (stream != NULL);
|
|
|
|
streams = program->streams;
|
|
pmt_index = stream->pmt_index;
|
|
pid = tsmux_stream_get_pid (stream);
|
|
|
|
if (pmt_index >= 0) {
|
|
/* Insert into streams with known indices */
|
|
for (i = 0; i < streams->len; i++) {
|
|
TsMuxStream *s = g_ptr_array_index (streams, i);
|
|
|
|
if (s->pmt_index < 0 || pmt_index < s->pmt_index) {
|
|
array_index = i;
|
|
GST_DEBUG ("PID 0x%04x: Using known-order index %d/%u",
|
|
pid, array_index, streams->len);
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
/* Insert after streams with known indices, sorted by PID */
|
|
for (i = 0; i < streams->len; i++) {
|
|
TsMuxStream *s = g_ptr_array_index (streams, i);
|
|
|
|
if (s->pmt_index < 0 && pid < tsmux_stream_get_pid (s)) {
|
|
array_index = i;
|
|
GST_DEBUG ("PID 0x%04x: Using PID-order index %d/%u",
|
|
pid, array_index, streams->len);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
g_ptr_array_insert (streams, array_index, stream);
|
|
program->pmt_changed = TRUE;
|
|
}
|
|
|
|
/**
|
|
* tsmux_program_set_pcr_stream:
|
|
* @program: a #TsMuxProgram
|
|
* @stream: a #TsMuxStream
|
|
*
|
|
* Set @stream as the PCR stream for @program, overwriting the previously
|
|
* configured PCR stream. When @stream is NULL, program will have no PCR stream
|
|
* configured.
|
|
*/
|
|
void
|
|
tsmux_program_set_pcr_stream (TsMuxProgram * program, TsMuxStream * stream)
|
|
{
|
|
g_return_if_fail (program != NULL);
|
|
|
|
if (program->pcr_stream == stream)
|
|
return;
|
|
|
|
if (program->pcr_stream != NULL)
|
|
tsmux_stream_pcr_unref (program->pcr_stream);
|
|
if (stream)
|
|
tsmux_stream_pcr_ref (stream);
|
|
program->pcr_stream = stream;
|
|
|
|
program->pmt_changed = TRUE;
|
|
}
|
|
|
|
/**
|
|
* tsmux_get_new_pid:
|
|
* @mux: a #TsMux
|
|
*
|
|
* Get a new free PID.
|
|
*
|
|
* Returns: a new free PID.
|
|
*/
|
|
guint16
|
|
tsmux_get_new_pid (TsMux * mux)
|
|
{
|
|
g_return_val_if_fail (mux != NULL, -1);
|
|
|
|
/* make sure this PID is free
|
|
* (and not taken by a specific earlier request) */
|
|
do {
|
|
mux->next_stream_pid++;
|
|
} while (tsmux_find_stream (mux, mux->next_stream_pid));
|
|
|
|
return mux->next_stream_pid;
|
|
}
|
|
|
|
/**
|
|
* tsmux_create_stream:
|
|
* @mux: a #TsMux
|
|
* @stream_type: the stream type
|
|
* @pid: the PID of the new stream.
|
|
*
|
|
* Create a new stream of @stream_type in the muxer session @mux.
|
|
*
|
|
* When @pid is set to #TSMUX_PID_AUTO, a new free PID will automatically
|
|
* be allocated for the new stream.
|
|
*
|
|
* Returns: a new #TsMuxStream.
|
|
*/
|
|
TsMuxStream *
|
|
tsmux_create_stream (TsMux * mux, guint stream_type, guint16 pid,
|
|
gchar * language)
|
|
{
|
|
TsMuxStream *stream;
|
|
guint16 new_pid;
|
|
|
|
g_return_val_if_fail (mux != NULL, NULL);
|
|
g_return_val_if_fail (mux->new_stream_func != NULL, NULL);
|
|
|
|
if (pid == TSMUX_PID_AUTO) {
|
|
new_pid = tsmux_get_new_pid (mux);
|
|
} else {
|
|
new_pid = pid & 0x1FFF;
|
|
}
|
|
|
|
/* Ensure we're not creating a PID collision */
|
|
if (tsmux_find_stream (mux, new_pid))
|
|
return NULL;
|
|
|
|
stream = mux->new_stream_func (new_pid, stream_type, mux->new_stream_data);
|
|
|
|
mux->streams = g_list_prepend (mux->streams, stream);
|
|
mux->nb_streams++;
|
|
|
|
if (language) {
|
|
strncpy (stream->language, language, 4);
|
|
stream->language[3] = 0;
|
|
} else {
|
|
stream->language[0] = 0;
|
|
}
|
|
|
|
return stream;
|
|
}
|
|
|
|
/**
|
|
* tsmux_find_stream:
|
|
* @mux: a #TsMux
|
|
* @pid: the PID to find.
|
|
*
|
|
* Find the stream associated with PID.
|
|
*
|
|
* Returns: a #TsMuxStream with @pid or NULL when the stream was not found.
|
|
*/
|
|
TsMuxStream *
|
|
tsmux_find_stream (TsMux * mux, guint16 pid)
|
|
{
|
|
TsMuxStream *found = NULL;
|
|
GList *cur;
|
|
|
|
g_return_val_if_fail (mux != NULL, NULL);
|
|
|
|
for (cur = mux->streams; cur; cur = cur->next) {
|
|
TsMuxStream *stream = (TsMuxStream *) cur->data;
|
|
|
|
if (tsmux_stream_get_pid (stream) == pid) {
|
|
found = stream;
|
|
break;
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
static gboolean
|
|
tsmux_program_remove_stream (TsMuxProgram * program, TsMuxStream * stream)
|
|
{
|
|
GPtrArray *streams = program->streams;
|
|
|
|
if (!g_ptr_array_remove (streams, stream)) {
|
|
g_warn_if_reached ();
|
|
return FALSE;
|
|
}
|
|
|
|
return streams->len == 0;
|
|
}
|
|
|
|
|
|
gboolean
|
|
tsmux_remove_stream (TsMux * mux, guint16 pid, TsMuxProgram * program)
|
|
{
|
|
GList *cur;
|
|
gboolean ret = FALSE;
|
|
|
|
g_return_val_if_fail (mux != NULL, FALSE);
|
|
|
|
for (cur = mux->streams; cur; cur = cur->next) {
|
|
TsMuxStream *stream = (TsMuxStream *) cur->data;
|
|
|
|
if (tsmux_stream_get_pid (stream) == pid) {
|
|
ret = tsmux_program_remove_stream (program, stream);
|
|
mux->streams = g_list_remove (mux->streams, stream);
|
|
tsmux_stream_free (stream);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ret)
|
|
tsmux_program_delete (mux, program);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
tsmux_get_buffer (TsMux * mux, GstBuffer ** buf)
|
|
{
|
|
g_return_val_if_fail (buf, FALSE);
|
|
|
|
if (G_UNLIKELY (!mux->alloc_func))
|
|
return FALSE;
|
|
|
|
mux->alloc_func (buf, mux->alloc_func_data);
|
|
|
|
if (!*buf)
|
|
return FALSE;
|
|
|
|
g_assert (gst_buffer_get_size (*buf) == TSMUX_PACKET_LENGTH);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
tsmux_packet_out (TsMux * mux, GstBuffer * buf, gint64 pcr)
|
|
{
|
|
if (G_UNLIKELY (mux->write_func == NULL)) {
|
|
if (buf)
|
|
gst_buffer_unref (buf);
|
|
return TRUE;
|
|
}
|
|
|
|
if (mux->bitrate) {
|
|
GST_BUFFER_PTS (buf) =
|
|
gst_util_uint64_scale (mux->n_bytes * 8, GST_SECOND, mux->bitrate);
|
|
|
|
/* Check and insert a PCR observation for each program if needed,
|
|
* but only for programs that have written their SI at least once,
|
|
* so the stream starts with PAT/PMT */
|
|
if (mux->first_pcr_ts != G_MININT64) {
|
|
GList *cur;
|
|
|
|
for (cur = mux->programs; cur; cur = cur->next) {
|
|
TsMuxProgram *program = (TsMuxProgram *) cur->data;
|
|
TsMuxStream *stream = program->pcr_stream;
|
|
gint64 cur_pcr, next_pcr, new_pcr;
|
|
|
|
if (!program->wrote_si)
|
|
continue;
|
|
|
|
cur_pcr = get_current_pcr (mux, 0);
|
|
next_pcr = get_next_pcr (mux, 0);
|
|
new_pcr = write_new_pcr (mux, stream, cur_pcr, next_pcr);
|
|
|
|
if (new_pcr != -1) {
|
|
GstBuffer *buf = NULL;
|
|
GstMapInfo map;
|
|
guint payload_len, payload_offs;
|
|
|
|
if (!tsmux_get_buffer (mux, &buf)) {
|
|
goto error;
|
|
}
|
|
|
|
gst_buffer_map (buf, &map, GST_MAP_READ);
|
|
tsmux_write_ts_header (mux, map.data, &stream->pi, &payload_len,
|
|
&payload_offs, 0);
|
|
gst_buffer_unmap (buf, &map);
|
|
|
|
stream->pi.flags &= TSMUX_PACKET_FLAG_PES_FULL_HEADER;
|
|
if (!tsmux_packet_out (mux, buf, new_pcr))
|
|
goto error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mux->n_bytes += gst_buffer_get_size (buf);
|
|
|
|
return mux->write_func (buf, mux->write_func_data, pcr);
|
|
|
|
error:
|
|
return FALSE;
|
|
}
|
|
|
|
/*
|
|
* adaptation_field() {
|
|
* adaptation_field_length 8 uimsbf
|
|
* if(adaptation_field_length >0) {
|
|
* discontinuity_indicator 1 bslbf
|
|
* random_access_indicator 1 bslbf
|
|
* elementary_stream_priority_indicator 1 bslbf
|
|
* PCR_flag 1 bslbf
|
|
* OPCR_flag 1 bslbf
|
|
* splicing_point_flag 1 bslbf
|
|
* transport_private_data_flag 1 bslbf
|
|
* adaptation_field_extension_flag 1 bslbf
|
|
* if(PCR_flag == '1') {
|
|
* program_clock_reference_base 33 uimsbf
|
|
* reserved 6 bslbf
|
|
* program_clock_reference_extension 9 uimsbf
|
|
* }
|
|
* if(OPCR_flag == '1') {
|
|
* original_program_clock_reference_base 33 uimsbf
|
|
* reserved 6 bslbf
|
|
* original_program_clock_reference_extension 9 uimsbf
|
|
* }
|
|
* if (splicing_point_flag == '1') {
|
|
* splice_countdown 8 tcimsbf
|
|
* }
|
|
* if(transport_private_data_flag == '1') {
|
|
* transport_private_data_length 8 uimsbf
|
|
* for (i=0; i<transport_private_data_length;i++){
|
|
* private_data_byte 8 bslbf
|
|
* }
|
|
* }
|
|
* if (adaptation_field_extension_flag == '1' ) {
|
|
* adaptation_field_extension_length 8 uimsbf
|
|
* ltw_flag 1 bslbf
|
|
* piecewise_rate_flag 1 bslbf
|
|
* seamless_splice_flag 1 bslbf
|
|
* reserved 5 bslbf
|
|
* if (ltw_flag == '1') {
|
|
* ltw_valid_flag 1 bslbf
|
|
* ltw_offset 15 uimsbf
|
|
* }
|
|
* if (piecewise_rate_flag == '1') {
|
|
* reserved 2 bslbf
|
|
* piecewise_rate 22 uimsbf
|
|
* }
|
|
* if (seamless_splice_flag == '1'){
|
|
* splice_type 4 bslbf
|
|
* DTS_next_AU[32..30] 3 bslbf
|
|
* marker_bit 1 bslbf
|
|
* DTS_next_AU[29..15] 15 bslbf
|
|
* marker_bit 1 bslbf
|
|
* DTS_next_AU[14..0] 15 bslbf
|
|
* marker_bit 1 bslbf
|
|
* }
|
|
* for ( i=0;i<N;i++) {
|
|
* reserved 8 bslbf
|
|
* }
|
|
* }
|
|
* for (i=0;i<N;i++){
|
|
* stuffing_byte 8 bslbf
|
|
* }
|
|
* }
|
|
* }
|
|
*/
|
|
static gboolean
|
|
tsmux_write_adaptation_field (guint8 * buf,
|
|
TsMuxPacketInfo * pi, guint8 min_length, guint8 * written)
|
|
{
|
|
guint8 pos = 2;
|
|
guint8 flags = 0;
|
|
|
|
g_assert (min_length <= TSMUX_PAYLOAD_LENGTH);
|
|
|
|
/* Write out all the fields from the packet info only if the
|
|
* user set the flag to request the adaptation field - if the flag
|
|
* isn't set, we're just supposed to write stuffing bytes */
|
|
if (pi->flags & TSMUX_PACKET_FLAG_ADAPTATION) {
|
|
TS_DEBUG ("writing adaptation fields");
|
|
if (pi->flags & TSMUX_PACKET_FLAG_DISCONT)
|
|
flags |= 0x80;
|
|
if (pi->flags & TSMUX_PACKET_FLAG_RANDOM_ACCESS)
|
|
flags |= 0x40;
|
|
if (pi->flags & TSMUX_PACKET_FLAG_PRIORITY)
|
|
flags |= 0x20;
|
|
if (pi->flags & TSMUX_PACKET_FLAG_WRITE_PCR) {
|
|
guint64 pcr_base;
|
|
guint32 pcr_ext;
|
|
|
|
pcr_base = (pi->pcr / 300);
|
|
pcr_ext = (pi->pcr % 300);
|
|
|
|
flags |= 0x10;
|
|
TS_DEBUG ("Writing PCR %" G_GUINT64_FORMAT " + ext %u", pcr_base,
|
|
pcr_ext);
|
|
buf[pos++] = (pcr_base >> 25) & 0xff;
|
|
buf[pos++] = (pcr_base >> 17) & 0xff;
|
|
buf[pos++] = (pcr_base >> 9) & 0xff;
|
|
buf[pos++] = (pcr_base >> 1) & 0xff;
|
|
buf[pos++] = ((pcr_base << 7) & 0x80) | 0x7e | ((pcr_ext >> 8) & 0x01); /* set 6 reserve bits to 1 */
|
|
buf[pos++] = (pcr_ext) & 0xff;
|
|
}
|
|
if (pi->flags & TSMUX_PACKET_FLAG_WRITE_OPCR) {
|
|
guint64 opcr_base;
|
|
guint32 opcr_ext;
|
|
|
|
opcr_base = (pi->opcr / 300);
|
|
opcr_ext = (pi->opcr % 300);
|
|
|
|
flags |= 0x08;
|
|
TS_DEBUG ("Writing OPCR");
|
|
buf[pos++] = (opcr_base >> 25) & 0xff;
|
|
buf[pos++] = (opcr_base >> 17) & 0xff;
|
|
buf[pos++] = (opcr_base >> 9) & 0xff;
|
|
buf[pos++] = (opcr_base >> 1) & 0xff;
|
|
buf[pos++] = ((opcr_base << 7) & 0x80) | 0x7e | ((opcr_ext >> 8) & 0x01); /* set 6 reserve bits to 1 */
|
|
buf[pos++] = (opcr_ext) & 0xff;
|
|
}
|
|
if (pi->flags & TSMUX_PACKET_FLAG_WRITE_SPLICE) {
|
|
flags |= 0x04;
|
|
buf[pos++] = pi->splice_countdown;
|
|
}
|
|
if (pi->private_data_len > 0) {
|
|
flags |= 0x02;
|
|
/* Private data to write, ensure we have enough room */
|
|
if ((1 + pi->private_data_len) > (TSMUX_PAYLOAD_LENGTH - pos))
|
|
return FALSE;
|
|
buf[pos++] = pi->private_data_len;
|
|
memcpy (&(buf[pos]), pi->private_data, pi->private_data_len);
|
|
pos += pi->private_data_len;
|
|
TS_DEBUG ("%u bytes of private data", pi->private_data_len);
|
|
}
|
|
if (pi->flags & TSMUX_PACKET_FLAG_WRITE_ADAPT_EXT) {
|
|
flags |= 0x01;
|
|
TS_DEBUG ("FIXME: write Adaptation extension");
|
|
/* Write an empty extension for now */
|
|
buf[pos++] = 1;
|
|
buf[pos++] = 0x1f; /* lower 5 bits are reserved, and should be all 1 */
|
|
}
|
|
}
|
|
/* Write the flags at the start */
|
|
buf[1] = flags;
|
|
|
|
/* Stuffing bytes if needed */
|
|
while (pos < min_length)
|
|
buf[pos++] = 0xff;
|
|
|
|
/* Write the adaptation field length, which doesn't include its own byte */
|
|
buf[0] = pos - 1;
|
|
|
|
if (written)
|
|
*written = pos;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
tsmux_write_ts_header (TsMux * mux, guint8 * buf, TsMuxPacketInfo * pi,
|
|
guint * payload_len_out, guint * payload_offset_out, guint stream_avail)
|
|
{
|
|
guint8 *tmp;
|
|
guint8 adaptation_flag = 0;
|
|
guint8 adapt_min_length = 0;
|
|
guint8 adapt_len = 0;
|
|
guint payload_len;
|
|
gboolean write_adapt = FALSE;
|
|
|
|
/* Sync byte */
|
|
buf[0] = TSMUX_SYNC_BYTE;
|
|
|
|
TS_DEBUG ("PID 0x%04x, counter = 0x%01x, %u bytes avail", pi->pid,
|
|
mux->pid_packet_counts[pi->pid] & 0x0f, stream_avail);
|
|
|
|
/* 3 bits:
|
|
* transport_error_indicator
|
|
* payload_unit_start_indicator
|
|
* transport_priority: (00)
|
|
* 13 bits: PID
|
|
*/
|
|
tmp = buf + 1;
|
|
if (pi->packet_start_unit_indicator) {
|
|
tsmux_put16 (&tmp, 0x4000 | pi->pid);
|
|
} else
|
|
tsmux_put16 (&tmp, pi->pid);
|
|
|
|
/* 2 bits: scrambling_control (NOT SUPPORTED) (00)
|
|
* 2 bits: adaptation field control (1x has_adaptation_field | x1 has_payload)
|
|
* 4 bits: continuity counter (xxxx)
|
|
*/
|
|
|
|
if (pi->flags & TSMUX_PACKET_FLAG_ADAPTATION) {
|
|
write_adapt = TRUE;
|
|
}
|
|
|
|
if (stream_avail < TSMUX_PAYLOAD_LENGTH) {
|
|
/* Need an adaptation field regardless for stuffing */
|
|
adapt_min_length = TSMUX_PAYLOAD_LENGTH - stream_avail;
|
|
write_adapt = TRUE;
|
|
}
|
|
|
|
if (write_adapt) {
|
|
gboolean res;
|
|
|
|
/* Flag the adaptation field presence */
|
|
adaptation_flag |= 0x20;
|
|
res = tsmux_write_adaptation_field (buf + TSMUX_HEADER_LENGTH,
|
|
pi, adapt_min_length, &adapt_len);
|
|
if (G_UNLIKELY (res == FALSE))
|
|
return FALSE;
|
|
|
|
/* Should have written at least the number of bytes we requested */
|
|
g_assert (adapt_len >= adapt_min_length);
|
|
}
|
|
|
|
/* The amount of packet data we wrote is the remaining space after
|
|
* the adaptation field */
|
|
*payload_len_out = payload_len = TSMUX_PAYLOAD_LENGTH - adapt_len;
|
|
*payload_offset_out = TSMUX_HEADER_LENGTH + adapt_len;
|
|
|
|
/* Now if we are going to write out some payload, flag that fact */
|
|
if (payload_len > 0 && stream_avail > 0) {
|
|
/* Flag the presence of a payload */
|
|
adaptation_flag |= 0x10;
|
|
|
|
/* We must have enough data to fill the payload, or some calculation
|
|
* went wrong */
|
|
g_assert (payload_len <= stream_avail);
|
|
|
|
/* Packet with payload, increment the continuity counter */
|
|
mux->pid_packet_counts[pi->pid]++;
|
|
}
|
|
|
|
adaptation_flag |= mux->pid_packet_counts[pi->pid] & 0x0f;
|
|
|
|
/* Write the byte of transport_scrambling_control, adaptation_field_control
|
|
* + continuity counter out */
|
|
buf[3] = adaptation_flag;
|
|
|
|
|
|
if (write_adapt) {
|
|
TS_DEBUG ("Adaptation field of size >= %d + %d bytes payload",
|
|
adapt_len, payload_len);
|
|
} else {
|
|
TS_DEBUG ("Payload of %d bytes only", payload_len);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* The unused_arg is needed for g_hash_table_foreach() */
|
|
static gboolean
|
|
tsmux_section_write_packet (gpointer unused_arg,
|
|
TsMuxSection * section, TsMux * mux)
|
|
{
|
|
GstBuffer *section_buffer;
|
|
GstBuffer *packet_buffer = NULL;
|
|
GstMemory *mem;
|
|
guint8 *packet;
|
|
guint8 *data;
|
|
gsize data_size = 0;
|
|
gsize payload_written;
|
|
guint len = 0, offset = 0, payload_len = 0;
|
|
guint extra_alloc_bytes = 0;
|
|
|
|
g_return_val_if_fail (section != NULL, FALSE);
|
|
g_return_val_if_fail (mux != NULL, FALSE);
|
|
|
|
/* Mark the start of new PES unit */
|
|
section->pi.packet_start_unit_indicator = TRUE;
|
|
|
|
data = gst_mpegts_section_packetize (section->section, &data_size);
|
|
|
|
if (!data) {
|
|
TS_DEBUG ("Could not packetize section");
|
|
return FALSE;
|
|
}
|
|
|
|
/* Mark payload data size */
|
|
section->pi.stream_avail = data_size;
|
|
payload_written = 0;
|
|
|
|
/* Wrap section data in a buffer without free function.
|
|
The data will be freed when the GstMpegtsSection is destroyed. */
|
|
section_buffer = gst_buffer_new_wrapped_full (GST_MEMORY_FLAG_READONLY,
|
|
data, data_size, 0, data_size, NULL, NULL);
|
|
|
|
TS_DEBUG ("Section buffer with size %" G_GSIZE_FORMAT " created",
|
|
gst_buffer_get_size (section_buffer));
|
|
|
|
while (section->pi.stream_avail > 0) {
|
|
|
|
packet = g_malloc (TSMUX_PACKET_LENGTH);
|
|
|
|
if (section->pi.packet_start_unit_indicator) {
|
|
/* Wee need room for a pointer byte */
|
|
section->pi.stream_avail++;
|
|
|
|
if (!tsmux_write_ts_header (mux, packet, §ion->pi, &len, &offset,
|
|
section->pi.stream_avail))
|
|
goto fail;
|
|
|
|
/* Write the pointer byte */
|
|
packet[offset++] = 0x00;
|
|
payload_len = len - 1;
|
|
|
|
} else {
|
|
if (!tsmux_write_ts_header (mux, packet, §ion->pi, &len, &offset,
|
|
section->pi.stream_avail))
|
|
goto fail;
|
|
payload_len = len;
|
|
}
|
|
|
|
/* Wrap the TS header and adaption field in a GstMemory */
|
|
mem = gst_memory_new_wrapped (GST_MEMORY_FLAG_READONLY,
|
|
packet, TSMUX_PACKET_LENGTH, 0, offset, packet, g_free);
|
|
|
|
TS_DEBUG ("Creating packet buffer at offset "
|
|
"%" G_GSIZE_FORMAT " with length %u", payload_written, payload_len);
|
|
|
|
/* If in M2TS mode, we will need to resize to 4 bytes after the end
|
|
of the buffer. For performance reasons, we will now try to include
|
|
4 extra bytes from the source buffer, then resize down, to avoid
|
|
having an extra 4 byte GstMemory appended. If the source buffer
|
|
does not have enough data for this, a new GstMemory will be used */
|
|
if (gst_buffer_get_size (section_buffer) - (payload_written +
|
|
payload_len) >= 4) {
|
|
/* enough space */
|
|
extra_alloc_bytes = 4;
|
|
} else {
|
|
extra_alloc_bytes = 0;
|
|
}
|
|
packet_buffer = gst_buffer_copy_region (section_buffer, GST_BUFFER_COPY_ALL,
|
|
payload_written, payload_len + extra_alloc_bytes);
|
|
|
|
/* Prepend the header to the section data */
|
|
gst_buffer_prepend_memory (packet_buffer, mem);
|
|
|
|
/* add an extra 4 bytes if it could not be reserved already */
|
|
if (extra_alloc_bytes == 4) {
|
|
/* we allocated those already, resize */
|
|
gst_buffer_set_size (packet_buffer,
|
|
gst_buffer_get_size (packet_buffer) - extra_alloc_bytes);
|
|
} else {
|
|
void *ptr = g_malloc (4);
|
|
GstMemory *extra =
|
|
gst_memory_new_wrapped (GST_MEMORY_FLAG_READONLY, ptr, 4, 0, 0, ptr,
|
|
g_free);
|
|
gst_buffer_append_memory (packet_buffer, extra);
|
|
}
|
|
|
|
TS_DEBUG ("Writing %d bytes to section. %d bytes remaining",
|
|
len, section->pi.stream_avail - len);
|
|
|
|
/* Push the packet without PCR */
|
|
if (G_UNLIKELY (!tsmux_packet_out (mux, packet_buffer, -1))) {
|
|
/* Buffer given away */
|
|
packet_buffer = NULL;
|
|
goto fail;
|
|
}
|
|
|
|
packet_buffer = NULL;
|
|
section->pi.stream_avail -= len;
|
|
payload_written += payload_len;
|
|
section->pi.packet_start_unit_indicator = FALSE;
|
|
}
|
|
|
|
gst_buffer_unref (section_buffer);
|
|
|
|
return TRUE;
|
|
|
|
fail:
|
|
g_free (packet);
|
|
if (section_buffer)
|
|
gst_buffer_unref (section_buffer);
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* tsmux_send_section:
|
|
* @mux: a #TsMux
|
|
* @section: (transfer full): a #GstMpegtsSection to add
|
|
*
|
|
* Send a @section immediately on the stream.
|
|
*
|
|
* Returns: %TRUE on success, %FALSE otherwise
|
|
*/
|
|
gboolean
|
|
tsmux_send_section (TsMux * mux, GstMpegtsSection * section)
|
|
{
|
|
gboolean ret;
|
|
TsMuxSection tsmux_section;
|
|
|
|
g_return_val_if_fail (mux != NULL, FALSE);
|
|
g_return_val_if_fail (section != NULL, FALSE);
|
|
|
|
memset (&tsmux_section, 0, sizeof (tsmux_section));
|
|
|
|
GST_DEBUG ("Sending mpegts section with type %d to mux",
|
|
section->section_type);
|
|
|
|
tsmux_section.section = section;
|
|
tsmux_section.pi.pid = section->pid;
|
|
|
|
ret = tsmux_section_write_packet (NULL, &tsmux_section, mux);
|
|
gst_mpegts_section_unref (section);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
tsmux_write_si (TsMux * mux)
|
|
{
|
|
g_hash_table_foreach (mux->si_sections,
|
|
(GHFunc) tsmux_section_write_packet, mux);
|
|
|
|
mux->si_changed = FALSE;
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
static void
|
|
tsmux_write_null_ts_header (guint8 * buf)
|
|
{
|
|
*buf++ = TSMUX_SYNC_BYTE;
|
|
*buf++ = 0x1f;
|
|
*buf++ = 0xff;
|
|
*buf++ = 0x10;
|
|
}
|
|
|
|
static gint64
|
|
ts_to_pcr (gint64 ts)
|
|
{
|
|
if (ts == G_MININT64) {
|
|
return 0;
|
|
}
|
|
|
|
return (ts - TSMUX_PCR_OFFSET) * (TSMUX_SYS_CLOCK_FREQ / TSMUX_CLOCK_FREQ);
|
|
}
|
|
|
|
/* Calculate the PCR to write into the current packet */
|
|
static gint64
|
|
get_current_pcr (TsMux * mux, gint64 cur_ts)
|
|
{
|
|
if (!mux->bitrate)
|
|
return ts_to_pcr (cur_ts);
|
|
|
|
if (mux->first_pcr_ts == G_MININT64) {
|
|
g_assert (cur_ts != G_MININT64);
|
|
mux->first_pcr_ts = cur_ts;
|
|
GST_DEBUG ("First PCR offset is %" G_GUINT64_FORMAT, cur_ts);
|
|
}
|
|
|
|
return ts_to_pcr (mux->first_pcr_ts) +
|
|
gst_util_uint64_scale ((mux->n_bytes + PCR_BYTE_OFFSET) * 8,
|
|
TSMUX_SYS_CLOCK_FREQ, mux->bitrate);
|
|
}
|
|
|
|
/* Predict the PCR at the next packet if possible */
|
|
static gint64
|
|
get_next_pcr (TsMux * mux, gint64 cur_ts)
|
|
{
|
|
if (!mux->bitrate)
|
|
return ts_to_pcr (cur_ts);
|
|
|
|
if (mux->first_pcr_ts == G_MININT64) {
|
|
g_assert (cur_ts != G_MININT64);
|
|
mux->first_pcr_ts = cur_ts;
|
|
GST_DEBUG ("First PCR offset is %" G_GUINT64_FORMAT, cur_ts);
|
|
}
|
|
|
|
return ts_to_pcr (mux->first_pcr_ts) +
|
|
gst_util_uint64_scale ((mux->n_bytes + TSMUX_PACKET_LENGTH +
|
|
PCR_BYTE_OFFSET) * 8, TSMUX_SYS_CLOCK_FREQ, mux->bitrate);
|
|
}
|
|
|
|
static gint64
|
|
write_new_pcr (TsMux * mux, TsMuxStream * stream, gint64 cur_pcr,
|
|
gint64 next_pcr)
|
|
{
|
|
if (stream->next_pcr == -1 || next_pcr > stream->next_pcr) {
|
|
stream->pi.flags |=
|
|
TSMUX_PACKET_FLAG_ADAPTATION | TSMUX_PACKET_FLAG_WRITE_PCR;
|
|
stream->pi.pcr = cur_pcr;
|
|
|
|
if (mux->bitrate && stream->next_pcr != -1 && cur_pcr >= stream->next_pcr) {
|
|
GST_WARNING ("Writing PCR %" G_GUINT64_FORMAT " missed the target %"
|
|
G_GUINT64_FORMAT " by %f ms", cur_pcr, stream->next_pcr,
|
|
(double) (cur_pcr - stream->next_pcr) / 27000.0);
|
|
}
|
|
/* Next PCR deadline is now plus the scheduled interval */
|
|
stream->next_pcr = cur_pcr + mux->pcr_interval * 300;
|
|
} else {
|
|
cur_pcr = -1;
|
|
}
|
|
|
|
return cur_pcr;
|
|
}
|
|
|
|
static gboolean
|
|
rewrite_si (TsMux * mux, gint64 cur_ts)
|
|
{
|
|
gboolean write_pat;
|
|
gboolean write_si;
|
|
GList *cur;
|
|
gint64 next_pcr;
|
|
|
|
next_pcr = get_next_pcr (mux, cur_ts);
|
|
|
|
/* check if we need to rewrite pat */
|
|
if (mux->next_pat_pcr == -1 || mux->pat_changed)
|
|
write_pat = TRUE;
|
|
else if (next_pcr > mux->next_pat_pcr)
|
|
write_pat = TRUE;
|
|
else
|
|
write_pat = FALSE;
|
|
|
|
if (write_pat) {
|
|
if (mux->next_pat_pcr == -1)
|
|
mux->next_pat_pcr = next_pcr + mux->pat_interval * 300;
|
|
else
|
|
mux->next_pat_pcr += mux->pat_interval * 300;
|
|
|
|
if (!tsmux_write_pat (mux))
|
|
return FALSE;
|
|
|
|
next_pcr = get_next_pcr (mux, cur_ts);
|
|
}
|
|
|
|
/* check if we need to rewrite sit */
|
|
if (mux->next_si_pcr == -1 || mux->si_changed)
|
|
write_si = TRUE;
|
|
else if (next_pcr > mux->next_si_pcr)
|
|
write_si = TRUE;
|
|
else
|
|
write_si = FALSE;
|
|
|
|
if (write_si) {
|
|
if (mux->next_si_pcr == -1)
|
|
mux->next_si_pcr = next_pcr + mux->si_interval * 300;
|
|
else
|
|
mux->next_si_pcr += mux->si_interval * 300;
|
|
|
|
if (!tsmux_write_si (mux))
|
|
return FALSE;
|
|
|
|
next_pcr = get_current_pcr (mux, cur_ts);
|
|
}
|
|
|
|
/* check if we need to rewrite any of the current pmts */
|
|
for (cur = mux->programs; cur; cur = cur->next) {
|
|
TsMuxProgram *program = (TsMuxProgram *) cur->data;
|
|
gboolean write_pmt;
|
|
|
|
if (program->next_pmt_pcr == -1 || program->pmt_changed)
|
|
write_pmt = TRUE;
|
|
else if (next_pcr > program->next_pmt_pcr)
|
|
write_pmt = TRUE;
|
|
else
|
|
write_pmt = FALSE;
|
|
|
|
if (write_pmt) {
|
|
if (program->next_pmt_pcr == -1)
|
|
program->next_pmt_pcr = next_pcr + program->pmt_interval * 300;
|
|
else
|
|
program->next_pmt_pcr += program->pmt_interval * 300;
|
|
|
|
if (!tsmux_write_pmt (mux, program))
|
|
return FALSE;
|
|
|
|
next_pcr = get_current_pcr (mux, cur_ts);
|
|
}
|
|
|
|
if (program->scte35_pid != 0) {
|
|
gboolean write_scte_null = FALSE;
|
|
if (program->next_scte35_pcr == -1)
|
|
write_scte_null = TRUE;
|
|
else if (next_pcr > program->next_scte35_pcr)
|
|
write_scte_null = TRUE;
|
|
|
|
if (write_scte_null) {
|
|
GST_DEBUG ("next scte35 pcr %" G_GINT64_FORMAT,
|
|
program->next_scte35_pcr);
|
|
if (program->next_scte35_pcr == -1)
|
|
program->next_scte35_pcr =
|
|
next_pcr + program->scte35_null_interval * 300;
|
|
else
|
|
program->next_scte35_pcr += program->scte35_null_interval * 300;
|
|
GST_DEBUG ("next scte35 NOW pcr %" G_GINT64_FORMAT,
|
|
program->next_scte35_pcr);
|
|
|
|
if (!tsmux_write_scte_null (mux, program))
|
|
return FALSE;
|
|
|
|
next_pcr = get_current_pcr (mux, cur_ts);
|
|
}
|
|
}
|
|
|
|
program->wrote_si = TRUE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
pad_stream (TsMux * mux, TsMuxStream * stream, gint64 cur_ts)
|
|
{
|
|
guint64 bitrate;
|
|
GstBuffer *buf = NULL;
|
|
GstMapInfo map;
|
|
gboolean ret = TRUE;
|
|
GstClockTimeDiff diff;
|
|
guint64 start_n_bytes;
|
|
|
|
if (!mux->bitrate)
|
|
goto done;
|
|
|
|
if (!GST_CLOCK_STIME_IS_VALID (cur_ts))
|
|
goto done;
|
|
|
|
if (!GST_CLOCK_STIME_IS_VALID (stream->first_ts))
|
|
stream->first_ts = cur_ts;
|
|
|
|
diff = GST_CLOCK_DIFF (stream->first_ts, cur_ts);
|
|
if (diff == 0)
|
|
goto done;
|
|
|
|
start_n_bytes = mux->n_bytes;
|
|
do {
|
|
GST_LOG ("Transport stream bitrate: %" G_GUINT64_FORMAT " over %"
|
|
G_GUINT64_FORMAT " bytes, duration %" GST_TIME_FORMAT,
|
|
gst_util_uint64_scale (mux->n_bytes * 8, TSMUX_CLOCK_FREQ, diff),
|
|
mux->n_bytes, GST_TIME_ARGS (diff * GST_SECOND / TSMUX_CLOCK_FREQ));
|
|
|
|
/* calculate what the overall bitrate will be if we add 1 more packet */
|
|
bitrate =
|
|
gst_util_uint64_scale ((mux->n_bytes + TSMUX_PACKET_LENGTH) * 8,
|
|
TSMUX_CLOCK_FREQ, diff);
|
|
|
|
if (bitrate <= mux->bitrate) {
|
|
gint64 new_pcr;
|
|
guint payload_len, payload_offs;
|
|
|
|
if (!tsmux_get_buffer (mux, &buf)) {
|
|
ret = FALSE;
|
|
goto done;
|
|
}
|
|
|
|
gst_buffer_map (buf, &map, GST_MAP_READ);
|
|
|
|
if ((new_pcr =
|
|
write_new_pcr (mux, stream, get_current_pcr (mux,
|
|
cur_ts), get_next_pcr (mux, cur_ts)) != -1)) {
|
|
GST_LOG ("Writing PCR-only packet on PID 0x%04x", stream->pi.pid);
|
|
tsmux_write_ts_header (mux, map.data, &stream->pi, &payload_len,
|
|
&payload_offs, 0);
|
|
} else {
|
|
GST_LOG ("Writing null stuffing packet");
|
|
if (!rewrite_si (mux, cur_ts)) {
|
|
ret = FALSE;
|
|
goto done;
|
|
}
|
|
tsmux_write_null_ts_header (map.data);
|
|
}
|
|
|
|
gst_buffer_unmap (buf, &map);
|
|
|
|
stream->pi.flags &= TSMUX_PACKET_FLAG_PES_FULL_HEADER;
|
|
|
|
if (!(ret = tsmux_packet_out (mux, buf, new_pcr)))
|
|
goto done;
|
|
}
|
|
} while (bitrate < mux->bitrate);
|
|
|
|
if (mux->n_bytes != start_n_bytes) {
|
|
GST_LOG ("Finished padding the mux");
|
|
}
|
|
|
|
done:
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* tsmux_write_stream_packet:
|
|
* @mux: a #TsMux
|
|
* @stream: a #TsMuxStream
|
|
*
|
|
* Write a packet of @stream.
|
|
*
|
|
* Returns: TRUE if the packet could be written.
|
|
*/
|
|
gboolean
|
|
tsmux_write_stream_packet (TsMux * mux, TsMuxStream * stream)
|
|
{
|
|
guint payload_len, payload_offs;
|
|
TsMuxPacketInfo *pi = &stream->pi;
|
|
gboolean res;
|
|
gint64 new_pcr = -1;
|
|
GstBuffer *buf = NULL;
|
|
GstMapInfo map;
|
|
|
|
g_return_val_if_fail (mux != NULL, FALSE);
|
|
g_return_val_if_fail (stream != NULL, FALSE);
|
|
|
|
if (tsmux_stream_is_pcr (stream)) {
|
|
gint64 cur_ts = CLOCK_BASE;
|
|
if (tsmux_stream_get_dts (stream) != G_MININT64)
|
|
cur_ts += tsmux_stream_get_dts (stream);
|
|
else
|
|
cur_ts += tsmux_stream_get_pts (stream);
|
|
|
|
if (!rewrite_si (mux, cur_ts))
|
|
goto fail;
|
|
|
|
if (!pad_stream (mux, stream, cur_ts))
|
|
goto fail;
|
|
|
|
new_pcr =
|
|
write_new_pcr (mux, stream, get_current_pcr (mux, cur_ts),
|
|
get_next_pcr (mux, cur_ts));
|
|
}
|
|
|
|
pi->packet_start_unit_indicator = tsmux_stream_at_pes_start (stream);
|
|
if (pi->packet_start_unit_indicator) {
|
|
tsmux_stream_initialize_pes_packet (stream);
|
|
if (stream->dts != G_MININT64)
|
|
stream->dts += CLOCK_BASE;
|
|
if (stream->pts != G_MININT64)
|
|
stream->pts += CLOCK_BASE;
|
|
}
|
|
pi->stream_avail = tsmux_stream_bytes_avail (stream);
|
|
|
|
/* obtain buffer */
|
|
if (!tsmux_get_buffer (mux, &buf))
|
|
return FALSE;
|
|
|
|
gst_buffer_map (buf, &map, GST_MAP_READ);
|
|
|
|
if (!tsmux_write_ts_header (mux, map.data, pi, &payload_len, &payload_offs,
|
|
pi->stream_avail))
|
|
goto fail;
|
|
|
|
|
|
if (!tsmux_stream_get_data (stream, map.data + payload_offs, payload_len))
|
|
goto fail;
|
|
|
|
gst_buffer_unmap (buf, &map);
|
|
|
|
GST_DEBUG ("Writing PES of size %d", (int) gst_buffer_get_size (buf));
|
|
res = tsmux_packet_out (mux, buf, new_pcr);
|
|
|
|
/* Reset all dynamic flags */
|
|
stream->pi.flags &= TSMUX_PACKET_FLAG_PES_FULL_HEADER;
|
|
|
|
return res;
|
|
|
|
/* ERRORS */
|
|
fail:
|
|
{
|
|
if (buf) {
|
|
gst_buffer_unmap (buf, &map);
|
|
gst_buffer_unref (buf);
|
|
}
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* tsmux_program_free:
|
|
* @program: a #TsMuxProgram
|
|
*
|
|
* Free the resources of @program. After this call @program can not be used
|
|
* anymore.
|
|
*/
|
|
void
|
|
tsmux_program_free (TsMuxProgram * program)
|
|
{
|
|
g_return_if_fail (program != NULL);
|
|
|
|
/* Free PMT section */
|
|
if (program->pmt.section)
|
|
gst_mpegts_section_unref (program->pmt.section);
|
|
if (program->scte35_null_section)
|
|
tsmux_section_free (program->scte35_null_section);
|
|
|
|
g_ptr_array_free (program->streams, TRUE);
|
|
g_slice_free (TsMuxProgram, program);
|
|
}
|
|
|
|
/**
|
|
* tsmux_program_set_pmt_pid:
|
|
* @program: A #TsmuxProgram
|
|
* @pmt_pid: PID to write PMT for this program
|
|
*/
|
|
void
|
|
tsmux_program_set_pmt_pid (TsMuxProgram * program, guint16 pmt_pid)
|
|
{
|
|
program->pmt_pid = pmt_pid;
|
|
}
|
|
|
|
static gint
|
|
compare_program_number (gconstpointer a, gconstpointer b)
|
|
{
|
|
const GstMpegtsPatProgram *pgm1 = *(const GstMpegtsPatProgram * const *) a;
|
|
const GstMpegtsPatProgram *pgm2 = *(const GstMpegtsPatProgram * const *) b;
|
|
gint num1 = pgm1->program_number, num2 = pgm2->program_number;
|
|
|
|
return num1 - num2;
|
|
}
|
|
|
|
static gboolean
|
|
tsmux_write_pat (TsMux * mux)
|
|
{
|
|
|
|
if (mux->pat_changed) {
|
|
/* program_association_section ()
|
|
* for (i = 0; i < N; i++) {
|
|
* program_number 16 uimsbf
|
|
* reserved 3 bslbf
|
|
* network_PID_or_program_map_PID 13 uimbsf
|
|
* }
|
|
* CRC_32 32 rbchof
|
|
*/
|
|
GList *cur;
|
|
GPtrArray *pat;
|
|
|
|
pat = gst_mpegts_pat_new ();
|
|
|
|
for (cur = mux->programs; cur; cur = cur->next) {
|
|
GstMpegtsPatProgram *pat_pgm;
|
|
TsMuxProgram *program = (TsMuxProgram *) cur->data;
|
|
|
|
pat_pgm = gst_mpegts_pat_program_new ();
|
|
pat_pgm->program_number = program->pgm_number;
|
|
pat_pgm->network_or_program_map_PID = program->pmt_pid;
|
|
|
|
g_ptr_array_add (pat, pat_pgm);
|
|
}
|
|
|
|
g_ptr_array_sort (pat, compare_program_number);
|
|
|
|
if (mux->pat.section)
|
|
gst_mpegts_section_unref (mux->pat.section);
|
|
|
|
mux->pat.section = gst_mpegts_section_from_pat (pat, mux->transport_id);
|
|
|
|
mux->pat.section->version_number = mux->pat_version++;
|
|
|
|
TS_DEBUG ("PAT has %d programs", mux->nb_programs);
|
|
mux->pat_changed = FALSE;
|
|
}
|
|
|
|
return tsmux_section_write_packet (NULL, &mux->pat, mux);
|
|
}
|
|
|
|
static gboolean
|
|
tsmux_write_pmt (TsMux * mux, TsMuxProgram * program)
|
|
{
|
|
|
|
if (program->pmt_changed) {
|
|
/* program_association_section ()
|
|
* reserved 3 bslbf
|
|
* PCR_PID 13 uimsbf
|
|
* reserved 4 bslbf
|
|
* program_info_length 12 uimsbf
|
|
* for (i = 0; i < N; i++)
|
|
* descriptor ()
|
|
*
|
|
* for (i = 0; i < N1; i++) {
|
|
* stream_type 8 uimsbf
|
|
* reserved 3 bslbf
|
|
* elementary_PID 13 uimbsf
|
|
* reserved 4 bslbf
|
|
* ES_info_length 12 uimbsf
|
|
* for (i = 0; i < N1; i++) {
|
|
* descriptor ();
|
|
* }
|
|
* }
|
|
*/
|
|
GstMpegtsDescriptor *descriptor;
|
|
GstMpegtsPMT *pmt;
|
|
#if 0
|
|
/* See note about bluray descriptors below */
|
|
guint8 desc[] = { 0x0F, 0xFF, 0xFC, 0xFC };
|
|
#endif
|
|
guint i;
|
|
|
|
pmt = gst_mpegts_pmt_new ();
|
|
|
|
if (program->pcr_stream == NULL)
|
|
pmt->pcr_pid = 0x1FFF;
|
|
else
|
|
pmt->pcr_pid = tsmux_stream_get_pid (program->pcr_stream);
|
|
|
|
#if 0
|
|
/* FIXME : These two descriptors should not be added in all PMT
|
|
* but only in "bluray-compatible" mpeg-ts output. I even have my
|
|
* doubt whether the DTCP descriptor is even needed */
|
|
descriptor = gst_mpegts_descriptor_from_registration ("HDMV", NULL, 0);
|
|
g_ptr_array_add (pmt->descriptors, descriptor);
|
|
|
|
/* DTCP descriptor, see
|
|
* http://www.dtcp.com/documents/dtcp/info-20150204-dtcp-v1-rev%201-71.pdf */
|
|
descriptor = gst_mpegts_descriptor_from_custom (0x88, desc, 4);
|
|
g_ptr_array_add (pmt->descriptors, descriptor);
|
|
#endif
|
|
|
|
/* Will SCTE-35 be potentially used ? */
|
|
if (program->scte35_pid != 0) {
|
|
descriptor = gst_mpegts_descriptor_from_registration ("CUEI", NULL, 0);
|
|
g_ptr_array_add (pmt->descriptors, descriptor);
|
|
}
|
|
|
|
/* Write out the entries */
|
|
for (i = 0; i < program->streams->len; i++) {
|
|
GstMpegtsPMTStream *pmt_stream;
|
|
TsMuxStream *stream = g_ptr_array_index (program->streams, i);
|
|
|
|
pmt_stream = gst_mpegts_pmt_stream_new ();
|
|
|
|
/* FIXME: Use API to retrieve this from the stream */
|
|
pmt_stream->stream_type = stream->stream_type;
|
|
pmt_stream->pid = tsmux_stream_get_pid (stream);
|
|
|
|
/* Write any ES descriptors needed */
|
|
tsmux_stream_get_es_descrs (stream, pmt_stream);
|
|
g_ptr_array_add (pmt->streams, pmt_stream);
|
|
}
|
|
|
|
/* Will SCTE-35 be potentially used ? */
|
|
if (program->scte35_pid != 0) {
|
|
GstMpegtsPMTStream *pmt_stream = gst_mpegts_pmt_stream_new ();
|
|
pmt_stream->stream_type = GST_MPEGTS_STREAM_TYPE_SCTE_SIT;
|
|
pmt_stream->pid = program->scte35_pid;
|
|
g_ptr_array_add (pmt->streams, pmt_stream);
|
|
}
|
|
|
|
TS_DEBUG ("PMT for program %d has %d streams",
|
|
program->pgm_number, program->streams->len);
|
|
|
|
pmt->program_number = program->pgm_number;
|
|
|
|
program->pmt.pi.pid = program->pmt_pid;
|
|
program->pmt_changed = FALSE;
|
|
|
|
if (program->pmt.section)
|
|
gst_mpegts_section_unref (program->pmt.section);
|
|
|
|
program->pmt.section = gst_mpegts_section_from_pmt (pmt, program->pmt_pid);
|
|
program->pmt.section->version_number = program->pmt_version++;
|
|
}
|
|
|
|
return tsmux_section_write_packet (NULL, &program->pmt, mux);
|
|
}
|
|
|
|
static gboolean
|
|
tsmux_write_scte_null (TsMux * mux, TsMuxProgram * program)
|
|
{
|
|
/* SCTE-35 NULL section is created when PID is set */
|
|
GST_LOG ("Writing SCTE NULL packet");
|
|
return tsmux_section_write_packet (NULL, program->scte35_null_section, mux);
|
|
}
|
|
|
|
void
|
|
tsmux_set_bitrate (TsMux * mux, guint64 bitrate)
|
|
{
|
|
mux->bitrate = bitrate;
|
|
}
|