gstreamer/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux-util.c
Edward Hervey 8f2d347559 hls: Relax webvtt checks
If no hour field is present (which is allowed), the remaining data can be less
than 15 character.

Fix time translation failures if the hour field wasn't present

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/2248>
2022-04-20 17:47:00 +00:00

1027 lines
31 KiB
C

/* GStreamer
* Copyright (C) 2016 Jan Schmidt <jan@centricular.com>
* Copyright (C) 2016 Tim-Philipp Müller <tim@centricular.com>
*
* gsthlsdemux-util.c:
*
* 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.
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <stdio.h>
#include <gmodule.h>
#include <gst/gst.h>
#include <gst/tag/tag.h>
#include <string.h>
#include "gsthlsdemux.h"
GST_DEBUG_CATEGORY_EXTERN (gst_hls_demux2_debug);
#define GST_CAT_DEFAULT gst_hls_demux2_debug
/* Mpeg-TS Packet */
#define TS_PACKET_SYNC_BYTE 0x47
#define TS_PACKET_TRANSPORT_ERROR_INDICATOR(packet) \
((packet)[1] & 0x80)
#define TS_PACKET_PAYLOAD_UNIT_START(packet) \
((packet)[1] & 0x40)
#define TS_PACKET_PID(packet) \
((guint16) ((packet)[1] & 0x1f) << 8 | (packet)[2])
#define TS_PACKET_TRANSPORT_SCRAMBLING_CONTROL(packet) \
((packet)[3] & 0xc0)
#define TS_PACKET_HAS_ADAPTATION_FIELD(packet) \
((packet)[3] & 0x20)
#define TS_PACKET_HAS_PAYLOAD(packet) \
((packet)[3] & 0x10)
#define TS_PACKET_CONTINUITY_COUNTER(pacet) \
((packet)[3] & 0x0f)
#define TS_PACKET_ADAPTATION_FIELD(packet) \
(TS_PACKET_HAS_ADAPTATION_FIELD(packet) ? \
(packet) + 4 : NULL)
/* Adaptation field size. Note: can be 0 */
#define TS_PACKET_ADAPTATION_FIELD_SIZE(packet) \
(packet)[4]
#define TS_PACKET_PAYLOAD_OFFSET(packet) \
(TS_PACKET_HAS_ADAPTATION_FIELD (packet) ? \
4 + TS_PACKET_ADAPTATION_FIELD_SIZE (packet) + 1 : \
4)
#define TS_PACKET_PAYLOAD(packet) \
(TS_PACKET_HAS_PAYLOAD (packet) ? \
(packet) + TS_PACKET_PAYLOAD_OFFSET(packet) : \
NULL)
/* PES Packet */
#define PES_IS_VALID(pes) ((pes)[0] == 0x00 && \
(pes)[1] == 0x00 && \
(pes)[2] == 0x01)
#define PES_STREAM_ID(pes) ((pes)[3])
#define PES_PACKET_LENGTH(pes) \
((guint16) (((pes)[4] << 8) | (pes)[5]))
#define PES_STREAM_TYPE_HAS_HEADER(stream_type) \
(stream_type != 0xac)
#define PES_HEADER_DATA_LENGTH(pes) ((pes)[8])
#define PES_PAYLOAD_DATA_OFFSET(pes) \
(9 + PES_HEADER_DATA_LENGTH (pes))
#define PES_HAS_PTS(pes) ((pes)[7] & 0x80)
#define PES_HAS_DTS(pes) ((pes)[7] & 0x40)
/* SI/PSI Packet */
#define TS_SECTION_POINTER(payload) ((payload)[0])
#define TS_PACKET_GET_SECTION(payload) ((payload) + TS_SECTION_POINTER(payload))
/* PAT section */
#define PAT_PROGRAM_OFFSET(pat, idx) \
(7 + (idx) * 4)
#define PAT_PROGRAM_PID_OFFSET(pat, idx) \
(PAT_PROGRAM_PID_OFFSET(pat,idx) + 2)
#define PAT_GET_PROGRAM_PID(pat, idx) \
(pat[ + PAT_PROGRAM_OFFSET(pat, idx) + 2)
static inline gboolean
read_ts (const guint8 * data, guint64 * target)
{
/* sync:4 == 00xx ! pts:3 ! 1 ! pts:15 ! 1 | pts:15 ! 1 */
if ((*data & 0x01) != 0x01)
return FALSE;
*target = ((guint64) (*data++ & 0x0E)) << 29;
*target |= ((guint64) (*data++)) << 22;
if ((*data & 0x01) != 0x01)
return FALSE;
*target |= ((guint64) (*data++ & 0xFE)) << 14;
*target |= ((guint64) (*data++)) << 7;
if ((*data & 0x01) != 0x01)
return FALSE;
*target |= ((guint64) (*data++ & 0xFE)) >> 1;
return TRUE;
}
#define PES_PTS_OFFSET(pes) (9)
#define PES_PTS(pes, dest) (read_ts ((pes) + PES_PTS_OFFSET(pes), dest))
#define PES_DTS_OFFSET(pes) (PES_HAS_PTS(pes) ? 9 + 5 : 9)
#define PES_DTS(pes, dest) (read_ts ((pes) + PES_DTS_OFFSET(pes), dest))
/* Check for sync byte, error_indicator == 0 and packet has payload.
* Adaptation control field (data[3] & 0x30) may be zero for TS packets with
* null PIDs. Still, these streams are valid TS streams (for null packets,
* AFC is supposed to be 0x1, but the spec also says decoders should just
* discard any packets with AFC = 0x00) */
#define IS_MPEGTS_HEADER(data) (data[0] == 0x47 && \
(data[1] & 0x80) == 0x00 && \
((data[3] & 0x30) != 0x00 || \
((data[3] & 0x30) == 0x00 && (data[1] & 0x1f) == 0x1f && (data[2] & 0xff) == 0xff)))
#define PCRTIME_TO_GSTTIME(t) (((t) * (guint64)1000) / 27)
#define MPEGTIME_TO_GSTTIME(t) (((t) * (guint64)100000) / 9)
static gboolean
have_ts_sync (const guint8 * data, guint size, guint packet_size, guint num)
{
while (num-- > 0) {
if (size < packet_size)
return FALSE;
if (!IS_MPEGTS_HEADER (data))
return FALSE;
data += packet_size;
size -= packet_size;
}
return TRUE;
}
#define GST_MPEGTS_TYPEFIND_MIN_HEADERS 4
static gint
find_offset (const guint8 * data, guint size, guint * out_packet_size)
{
guint sync_points = CLAMP (size / 188, GST_MPEGTS_TYPEFIND_MIN_HEADERS, 100);
guint off;
const gint packet_size = 188;
/* FIXME: check 192 as well, and maybe also 204, 208 */
for (off = 0; off < MIN (size, packet_size); ++off) {
if (have_ts_sync (data + off, size - off, packet_size, sync_points)) {
*out_packet_size = packet_size;
return off;
}
}
return -1;
}
static gboolean
handle_pmt (const guint8 * data, guint size, guint packet_size)
{
const guint8 *p = data;
guint32 hdr = GST_READ_UINT32_BE (p);
guint slen, pcr_pid, pilen;
GST_MEMDUMP ("PMT", data, size);
data = p + 4;
if ((hdr & 0x00000020) != 0) /* has_adaptation_field */
data += 1 + p[4]; /* adaptation_field_len */
data += 1 + data[0]; /* pointer_field */
if (data[0] != 0x02) /* table_id */
return FALSE;
//gst_util_dump_mem (data, 8);
/* we assume the entire PMT fits into a single packet and this is it */
if (data[6] != 0 || data[6] != data[7])
return FALSE;
slen = GST_READ_UINT16_BE (data + 1) & 0x0FFF;
if (slen > (gsize) (p + packet_size - (data + 1 + 2)) || slen < 5 + 2 + 4)
return FALSE;
data += 3 + 5;
slen -= 5; /* bytes after section_length field itself */
slen -= 4; /* crc at end */
pcr_pid = GST_READ_UINT16_BE (data) & 0x1fff;
if (pcr_pid != 0x1fff) {
GST_DEBUG ("pcr_pid: %04x", pcr_pid);
}
data += 2;
/* Skip global descriptors */
pilen = GST_READ_UINT16_BE (data + 1) & 0x0FFF;
data += 2 + pilen;
return FALSE;
}
static gboolean
pat_get_pmt_pid (const guint8 * data, guint size, guint packet_size,
gint * pmt_pid)
{
const guint8 *p = data;
guint32 hdr = GST_READ_UINT32_BE (p);
guint slen;
data = p + 4;
if ((hdr & 0x00000020) != 0) /* has_adaptation_field */
data += 1 + p[4]; /* adaptation_field_len */
data += 1 + data[0]; /* pointer_field */
if (data[0] != 0) /* table_id */
return FALSE;
/* we assume the entire PAT fits into a single packet and this is it */
if (data[6] != 0 || data[6] != data[7])
return FALSE;
slen = GST_READ_UINT16_BE (data + 1) & 0x0FFF;
if (slen > (gsize) (p + packet_size - (data + 1 + 2)) || slen < 5 + 4 + 4)
return FALSE;
data += 3 + 5;
slen -= 5; /* bytes after section_length field itself */
slen -= 4; /* crc at end */
while (slen >= 4) {
guint program_num = GST_READ_UINT16_BE (data);
guint val = GST_READ_UINT16_BE (data + 2) & 0x1fff;
if (program_num != 0) {
GST_DEBUG (" program %04x: pmt_pid : %04x", program_num, val);
*pmt_pid = val;
return TRUE;
}
data += 4;
slen -= 4;
}
return FALSE;
}
static GstClockTime
get_first_mpegts_time (const guint8 * data, gsize size, guint packet_size)
{
GstClockTime internal_time = GST_CLOCK_TIME_NONE;
const guint8 *p;
gint pmt_pid = -1;
for (p = data; size >= packet_size; p += packet_size, size -= packet_size) {
if (p[0] != TS_PACKET_SYNC_BYTE) {
GST_WARNING ("Lost sync");
break;
}
/* We only care about start packets which have some form of payload (pes or
section) */
if (TS_PACKET_PAYLOAD_UNIT_START (p) && TS_PACKET_HAS_PAYLOAD (p)) {
guint16 pid;
const guint8 *payload;
const guint8 *afc;
/* Skip packets which have error indicator set or are scrambled */
if (G_UNLIKELY (TS_PACKET_TRANSPORT_ERROR_INDICATOR (p) ||
TS_PACKET_TRANSPORT_SCRAMBLING_CONTROL (p)))
continue;
pid = TS_PACKET_PID (p);
payload = TS_PACKET_PAYLOAD (p);
afc = TS_PACKET_ADAPTATION_FIELD (p);
GST_LOG ("PID 0x%04x", pid);
if (afc && afc[0])
GST_MEMDUMP ("afc", afc, afc[0]);
GST_MEMDUMP ("payload", payload, 32);
if (pmt_pid != -1 && PES_IS_VALID (payload)) {
guint64 ts;
GstClockTime pts, dts;
pts = dts = GST_CLOCK_TIME_NONE;
GST_DEBUG ("PID 0x%04x stream_id 0x%02x PES start", pid,
PES_STREAM_ID (payload));
GST_MEMDUMP ("PES data", payload + PES_PAYLOAD_DATA_OFFSET (payload),
32);
/* Grab PTS/DTS */
if (PES_HAS_PTS (payload) && PES_PTS (payload, &ts)) {
pts = MPEGTIME_TO_GSTTIME (ts);
GST_LOG ("PID 0x%04x PTS %" G_GUINT64_FORMAT " %" GST_TIME_FORMAT,
pid, ts, GST_TIME_ARGS (pts));
}
if (PES_HAS_DTS (payload) && PES_DTS (payload, &ts)) {
dts = MPEGTIME_TO_GSTTIME (ts);
GST_LOG ("PID 0x%04x DTS %" G_GUINT64_FORMAT " %" GST_TIME_FORMAT,
pid, ts, GST_TIME_ARGS (dts));
}
/* Pick the lowest value */
if (GST_CLOCK_TIME_IS_VALID (dts)) {
if (GST_CLOCK_TIME_IS_VALID (pts)) {
/* Only take the PTS if it's lower than the dts and does not differ
* by more than a second (which would indicate bogus values) */
if (pts < dts && ABS (pts - dts) < GST_SECOND)
internal_time = pts;
else
internal_time = dts;
} else {
internal_time = dts;
}
goto out;
} else if (GST_CLOCK_TIME_IS_VALID (pts)) {
internal_time = pts;
goto out;
}
} else if (pid == 0x00) {
GST_DEBUG ("PAT !");
if (!pat_get_pmt_pid (p, packet_size, packet_size, &pmt_pid)) {
GST_WARNING ("Invalid PAT");
goto out;
}
} else if (pmt_pid != -1 && pid == pmt_pid) {
GST_DEBUG ("PMT !");
/* FIXME : Grab the list of *actual* elementary stream PID to make sure
* we have checked the first PTS of each stream (and not just the first
* one we saw, which might not be the smallest */
handle_pmt (p, packet_size, packet_size);
}
}
}
out:
return internal_time;
}
GstHLSParserResult
gst_hlsdemux_handle_content_mpegts (GstHLSDemux * demux,
GstHLSDemuxStream * hls_stream, gboolean draining, GstBuffer ** buffer)
{
GstMapInfo info;
gint offset;
const guint8 *data;
GstClockTime internal_time = GST_CLOCK_TIME_NONE;
guint packet_size;
gsize size;
if (!gst_buffer_map (*buffer, &info, GST_MAP_READ))
return GST_HLS_PARSER_RESULT_ERROR;
data = info.data;
size = info.size;
offset = find_offset (data, size, &packet_size);
if (offset < 0) {
gst_buffer_unmap (*buffer, &info);
return GST_HLS_PARSER_RESULT_ERROR;
}
GST_LOG ("TS packet start offset: %d", offset);
data += offset;
size -= offset;
internal_time = get_first_mpegts_time (data, size, packet_size);
GST_DEBUG_OBJECT (hls_stream, "Using internal time %" GST_TIME_FORMAT,
GST_TIME_ARGS (internal_time));
gst_buffer_unmap (*buffer, &info);
if (!GST_CLOCK_TIME_IS_VALID (internal_time))
return GST_HLS_PARSER_RESULT_NEED_MORE_DATA;
/* We have the first internal time, figure out if we are in sync or not */
return gst_hlsdemux_handle_internal_time (demux, hls_stream, internal_time);
}
GstHLSParserResult
gst_hlsdemux_handle_content_isobmff (GstHLSDemux * demux,
GstHLSDemuxStream * hls_stream, gboolean draining, GstBuffer ** buffer)
{
GstMapInfo info;
GstByteReader br, sub;
guint32 box_type;
guint header_size;
guint64 box_size;
GstHLSParserResult ret = GST_HLS_PARSER_RESULT_NEED_MORE_DATA;
GstClockTime smallest_ts = GST_CLOCK_TIME_NONE;
if (!gst_buffer_map (*buffer, &info, GST_MAP_READ))
return GST_HLS_PARSER_RESULT_ERROR;
gst_byte_reader_init (&br, info.data, info.size);
while (gst_byte_reader_get_remaining (&br) &&
gst_isoff_parse_box_header (&br, &box_type, NULL, &header_size,
&box_size)) {
GST_DEBUG ("box %" GST_FOURCC_FORMAT " size:%" G_GUINT64_FORMAT,
GST_FOURCC_ARGS (box_type), box_size);
GST_MEMDUMP ("box content", br.data + br.byte, MIN (256,
box_size - header_size));
switch (box_type) {
case GST_ISOFF_FOURCC_MOOV:
{
GstMoovBox *moov;
gst_byte_reader_get_sub_reader (&br, &sub, box_size - header_size);
moov = gst_isoff_moov_box_parse (&sub);
if (moov) {
GST_DEBUG ("Got moov box");
if (hls_stream->moov)
gst_isoff_moov_box_free (hls_stream->moov);
hls_stream->moov = moov;
}
break;
}
case GST_ISOFF_FOURCC_MOOF:
{
GstMoofBox *moof;
gst_byte_reader_get_sub_reader (&br, &sub, box_size - header_size);
moof = gst_isoff_moof_box_parse (&sub);
if (moof) {
guint i, j;
GST_DEBUG ("Got moof box");
/* Use the track information from stream->moov */
for (i = 0; i < hls_stream->moov->trak->len; i++) {
GstTrakBox *trak =
&g_array_index (hls_stream->moov->trak, GstTrakBox, i);
GST_LOG ("trak #%d %p", i, trak);
for (j = 0; j < moof->traf->len; j++) {
GstTrafBox *traf = &g_array_index (moof->traf, GstTrafBox, j);
if (traf->tfhd.track_id == trak->tkhd.track_id) {
GstClockTime ts = 0;
guint64 decode_time = traf->tfdt.decode_time;
if (decode_time != GST_CLOCK_TIME_NONE)
ts = gst_util_uint64_scale (decode_time, GST_SECOND,
trak->mdia.mdhd.timescale);
GST_LOG ("Found decode_time %" GST_TIME_FORMAT " for trak %d",
GST_TIME_ARGS (ts), traf->tfhd.track_id);
if (smallest_ts == GST_CLOCK_TIME_NONE || ts < smallest_ts)
smallest_ts = ts;
}
}
}
gst_isoff_moof_box_free (moof);
} else {
GST_WARNING ("Failed to parse moof");
}
if (smallest_ts != GST_CLOCK_TIME_NONE)
goto out;
break;
}
case GST_ISOFF_FOURCC_MDAT:
GST_DEBUG ("Reached `mdat`, returning");
goto out;
break;
default:
GST_LOG ("Skipping unhandled box %" GST_FOURCC_FORMAT,
GST_FOURCC_ARGS (box_type));
gst_byte_reader_skip (&br, box_size - header_size);
break;
}
}
out:
gst_buffer_unmap (*buffer, &info);
if (smallest_ts != GST_CLOCK_TIME_NONE) {
ret = gst_hlsdemux_handle_internal_time (demux, hls_stream, smallest_ts);
}
return ret;
}
GstHLSParserResult
gst_hlsdemux_handle_content_id3 (GstHLSDemux * demux,
GstHLSDemuxStream * hls_stream, gboolean draining, GstBuffer ** buffer)
{
GstMapInfo info;
guint32 tag_size;
gsize size;
GstTagList *taglist;
GstSample *priv_data = NULL;
GstBuffer *tag_buf;
guint64 pts;
GstHLSParserResult ret = GST_HLS_PARSER_RESULT_DONE;
GstClockTime internal;
/* We need at least 10 bytes, starting with "ID3" for the header */
size = gst_buffer_get_size (*buffer);
if (size < 10)
return GST_HLS_PARSER_RESULT_NEED_MORE_DATA;
/* Read the tag size */
tag_size = gst_tag_get_id3v2_tag_size (*buffer);
/* Check we've collected that much */
if (size < tag_size)
return GST_HLS_PARSER_RESULT_NEED_MORE_DATA;
/* Parse the tag */
taglist = gst_tag_list_from_id3v2_tag (*buffer);
if (taglist == NULL) {
return GST_HLS_PARSER_RESULT_ERROR; /* Invalid tag, stop trying */
}
/* Extract the timestamps */
if (!gst_tag_list_get_sample (taglist, GST_TAG_PRIVATE_DATA, &priv_data))
goto out;
if (!g_str_equal ("com.apple.streaming.transportStreamTimestamp",
gst_structure_get_string (gst_sample_get_info (priv_data), "owner")))
goto out;
/* OK, now as per section 3, the tag contains a 33-bit PCR inside a 64-bit
* BE-word */
tag_buf = gst_sample_get_buffer (priv_data);
if (tag_buf == NULL)
goto out;
if (!gst_buffer_map (tag_buf, &info, GST_MAP_READ))
goto out;
GST_MEMDUMP ("id3 tag", info.data, info.size);
pts = GST_READ_UINT64_BE (info.data);
internal = MPEGTIME_TO_GSTTIME (pts);
GST_LOG ("Got internal PTS from ID3: %" G_GUINT64_FORMAT " (%" GST_TIME_FORMAT
")", pts, GST_TIME_ARGS (internal));
gst_buffer_unmap (tag_buf, &info);
ret = gst_hlsdemux_handle_internal_time (demux, hls_stream, internal);
out:
if (priv_data)
gst_sample_unref (priv_data);
if (taglist)
gst_tag_list_unref (taglist);
return ret;
}
/* Grabs the next numerical value from the bytereader, skipping any spaces.
*
* It will stop/return at the next non-digit/non-space position */
static gboolean
byte_reader_get_next_uint_string (GstByteReader * br, guint * out)
{
guint value = 0;
gboolean res = FALSE;
while (gst_byte_reader_get_remaining (br)) {
guint8 d = gst_byte_reader_peek_uint8_unchecked (br);
if (g_ascii_isdigit (d)) {
value = value * 10 + (d - '0');
res = TRUE;
} else if (d != ' ' && d != '\t') {
/* we're done and not advancing */
break;
}
gst_byte_reader_skip_unchecked (br, 1);
}
if (res)
*out = value;
return res;
}
/* Grabs the next numerical value from the bytereader, skipping any spaces.
*
* It will stop/return at the next non-digit/non-space position */
static gboolean
byte_reader_get_next_uint64_string (GstByteReader * br, guint64 * out)
{
guint64 value = 0;
gboolean res = FALSE;
while (gst_byte_reader_get_remaining (br)) {
guint8 d = gst_byte_reader_peek_uint8_unchecked (br);
if (g_ascii_isdigit (d)) {
value = value * 10 + (d - '0');
res = TRUE;
} else if (d != ' ' && d != '\t') {
/* we're done and not advancing */
break;
}
gst_byte_reader_skip_unchecked (br, 1);
}
if (res)
*out = value;
return res;
}
static gboolean
parse_webvtt_time (GstByteReader * br, GstClockTime * t,
const gchar ** remainder)
{
GstClockTime val = 0;
gboolean res = FALSE;
while (!res && gst_byte_reader_get_remaining (br)) {
guint numval;
if (byte_reader_get_next_uint_string (br, &numval)) {
guint8 next = gst_byte_reader_peek_uint8_unchecked (br);
if (next == ':' || next == '.') {
/* value was hours, minutes or seconds */
val = val * 60 + numval;
gst_byte_reader_skip (br, 1);
} else {
/* Reached the milliseconds, convert to GstClockTime */
val = val * GST_SECOND + numval * GST_MSECOND;
res = TRUE;
}
}
}
if (res) {
*t = val;
if (remainder) {
if (gst_byte_reader_get_remaining (br))
*remainder = (const gchar *) gst_byte_reader_peek_data_unchecked (br);
else
*remainder = NULL;
}
}
return res;
}
static inline void
br_skipwhitespace (GstByteReader * br)
{
while (gst_byte_reader_get_remaining (br)) {
guint8 d = gst_byte_reader_peek_uint8_unchecked (br);
if (d != ' ' && d != '\t')
return;
gst_byte_reader_skip_unchecked (br, 1);
}
}
/* Returns TRUE if br starts with str
*
* Skips any spaces/tabs before and after str */
static gboolean
br_startswith (GstByteReader * br, const gchar * str, gboolean skip_ws)
{
guint len = strlen (str);
const guint8 *data;
if (skip_ws)
br_skipwhitespace (br);
if (!gst_byte_reader_peek_data (br, len, &data))
return FALSE;
if (strncmp ((gchar *) data, str, len))
return FALSE;
gst_byte_reader_skip_unchecked (br, len);
if (skip_ws)
br_skipwhitespace (br);
return TRUE;
}
static gboolean
gst_hls_demux_webvtt_read_x_timestamp_map (gchar * data, GstClockTime * local,
GstClockTime * mpegts)
{
GstByteReader br;
gst_byte_reader_init (&br, (guint8 *) data, strlen (data));
if (!br_startswith (&br, "X-TIMESTAMP-MAP=", FALSE))
return FALSE;
if (br_startswith (&br, "MPEGTS:", TRUE)) {
if (!byte_reader_get_next_uint64_string (&br, mpegts))
return FALSE;
/* Convert to GstClockTime */
*mpegts = MPEGTIME_TO_GSTTIME (*mpegts);
if (!br_startswith (&br, ",", TRUE))
return FALSE;
if (!br_startswith (&br, "LOCAL:", TRUE))
return FALSE;
if (!parse_webvtt_time (&br, local, NULL))
return FALSE;
} else if (br_startswith (&br, "LOCAL:", TRUE)) {
if (!parse_webvtt_time (&br, local, NULL))
return FALSE;
if (!br_startswith (&br, ",", TRUE))
return FALSE;
if (!br_startswith (&br, "MPEGTS:", TRUE))
return FALSE;
if (!byte_reader_get_next_uint64_string (&br, mpegts))
return FALSE;
/* Convert to GstClockTime */
*mpegts = MPEGTIME_TO_GSTTIME (*mpegts);
} else {
return FALSE;
}
GST_DEBUG ("local time:%" GST_TIME_FORMAT ", mpegts time:%" GST_TIME_FORMAT,
GST_TIME_ARGS (*local), GST_TIME_ARGS (*mpegts));
return TRUE;
}
static gboolean
utf8_string_contains_alnum (gchar * string)
{
gunichar c;
while ((c = g_utf8_get_char (string))) {
if (g_unichar_isalnum (c))
return TRUE;
string = g_utf8_next_char (string);
}
return FALSE;
}
#define T_H(t) ((t) / (GST_SECOND * 60 * 60))
#define T_M(t) ((t) / (GST_SECOND * 60) % 60)
#define T_S(t) ((t) / GST_SECOND % 60)
#define WEBVTT_TIME_FORMAT "02u:%02u:%02u.%03u"
#define WEBVTT_TIME_ARGS(t) \
(guint) ((t) / (GST_SECOND * 60 * 60)) , \
(guint) ((t) / (GST_SECOND * 60) % 60), \
(guint) ((t) / GST_SECOND % 60), \
(guint) ((t) / GST_MSECOND % 1000)
static gboolean
process_webvtt_cue_timing_setting_line (const gchar * input,
GstClockTime * start, GstClockTime * stop, const gchar ** cue_settings)
{
GstByteReader br;
gst_byte_reader_init (&br, (guint8 *) input, strlen (input));
/* Handle cue timing start */
if (!parse_webvtt_time (&br, start, NULL))
return FALSE;
/* --> */
if (gst_byte_reader_get_remaining (&br) < 12 ||
g_ascii_strncasecmp ((const gchar *)
gst_byte_reader_peek_data_unchecked (&br), "-->", 3))
return FALSE;
gst_byte_reader_skip (&br, 4);
/* Handle cue timing stop */
if (!parse_webvtt_time (&br, stop, cue_settings))
return FALSE;
return TRUE;
}
static GstClockTimeDiff
convert_webvtt_to_stream_time (GstHLSTimeMap * map, GstClockTime localtime,
GstClockTime mpegtime, GstClockTime vtt_value)
{
GstClockTimeDiff res;
if (localtime == GST_CLOCK_TIME_NONE || mpegtime == GST_CLOCK_TIME_NONE) {
GST_DEBUG ("No X-TIMESTAMP-MAP, assuming values are MPEG-TS values");
res = gst_hls_internal_to_stream_time (map, vtt_value);
/* VTT only uses positive values */
if (res < 0)
res = 0;
} else {
GST_DEBUG ("Converting %" GST_TIME_FORMAT,
GST_TIME_ARGS (vtt_value + mpegtime - localtime));
res =
gst_hls_internal_to_stream_time (map, vtt_value + mpegtime - localtime);
if (res == GST_CLOCK_STIME_NONE) {
GST_WARNING ("Couldn't convert value, using original value %"
GST_TIME_FORMAT, GST_TIME_ARGS (vtt_value));
res = vtt_value;
} else if (res < 0) {
res = 0;
}
}
return res;
}
GstHLSParserResult
gst_hlsdemux_handle_content_webvtt (GstHLSDemux * demux,
GstHLSDemuxStream * hls_stream, gboolean draining, GstBuffer ** buffer)
{
GstHLSParserResult ret = GST_HLS_PARSER_RESULT_DONE;
gchar *original_content;
guint i, nb;
gchar **original_lines;
GstClockTime localtime = GST_CLOCK_TIME_NONE;
GstClockTime mpegtime = GST_CLOCK_TIME_NONE;
GstClockTime low_stream_time = GST_CLOCK_STIME_NONE;
GstClockTime high_stream_time = GST_CLOCK_STIME_NONE;
gboolean found_timing = FALSE;
gboolean found_text = FALSE;
GPtrArray *builder;
GstM3U8MediaSegment *current_segment = hls_stream->current_segment;
GstClockTimeDiff segment_start, segment_end;
GstClockTimeDiff tolerance;
gboolean out_of_bounds = FALSE;
GstHLSTimeMap *map;
/* We only process full webvtt fragments */
if (!draining)
return GST_HLS_PARSER_RESULT_NEED_MORE_DATA;
original_content = gst_hls_buf_to_utf8_text (*buffer);
if (!original_content)
return GST_HLS_PARSER_RESULT_ERROR;
segment_start = current_segment->stream_time;
segment_end = segment_start + current_segment->duration;
tolerance = MAX (current_segment->duration / 2, 500 * GST_MSECOND);
map = gst_hls_find_time_map (demux, current_segment->discont_sequence);
builder = g_ptr_array_new_with_free_func (g_free);
original_lines = g_strsplit_set (original_content, "\n\r", 0);
nb = g_strv_length (original_lines);
for (i = 0; i < nb; i++) {
gchar *line = original_lines[i];
GST_LOG ("Line: %s", line);
if (g_str_has_prefix (line, "X-TIMESTAMP-MAP=")) {
if (!gst_hls_demux_webvtt_read_x_timestamp_map (line, &localtime,
&mpegtime)) {
GST_WARNING ("webvtt timestamp map isn't valid");
ret = GST_HLS_PARSER_RESULT_ERROR;
goto out;
}
g_ptr_array_add (builder, g_strdup (line));
} else if (strstr (line, " --> ")) {
GstClockTime start, stop;
const gchar *leftover;
if (process_webvtt_cue_timing_setting_line (line, &start, &stop,
&leftover)) {
GstClockTimeDiff start_stream, stop_stream;
gchar *newline;
GST_LOG ("Found time line %" GST_TIME_FORMAT " -> %" GST_TIME_FORMAT,
GST_TIME_ARGS (start), GST_TIME_ARGS (stop));
start_stream =
convert_webvtt_to_stream_time (map, localtime, mpegtime, start);
stop_stream =
convert_webvtt_to_stream_time (map, localtime, mpegtime, stop);
GST_LOG ("Stream time %" GST_STIME_FORMAT " --> %" GST_STIME_FORMAT,
GST_STIME_ARGS (start_stream), GST_STIME_ARGS (stop_stream));
if (stop_stream < (segment_start - tolerance) ||
start_stream > (segment_end + tolerance)) {
GST_WARNING ("Out of bounds");
out_of_bounds = TRUE;
}
if (low_stream_time == GST_CLOCK_STIME_NONE
|| stop_stream < low_stream_time)
low_stream_time = stop_stream;
if (high_stream_time == GST_CLOCK_STIME_NONE
|| start_stream > high_stream_time)
high_stream_time = start_stream;
/* Apply the stream presentation offset */
start_stream += hls_stream->presentation_offset;
stop_stream += hls_stream->presentation_offset;
/* Create the time-shifted WebVTT cue line */
if (leftover) {
newline =
g_strdup_printf ("%" WEBVTT_TIME_FORMAT " --> %"
WEBVTT_TIME_FORMAT " %s", WEBVTT_TIME_ARGS (start_stream),
WEBVTT_TIME_ARGS (stop_stream), leftover);
} else {
newline =
g_strdup_printf ("%" WEBVTT_TIME_FORMAT " --> %"
WEBVTT_TIME_FORMAT, WEBVTT_TIME_ARGS (start_stream),
WEBVTT_TIME_ARGS (stop_stream));
}
GST_LOG ("Generated line '%s'", newline);
g_ptr_array_add (builder, newline);
found_timing = TRUE;
} else {
GST_WARNING ("Failed to parse time line '%s'", line);
/* Abort ? */
}
} else if (found_timing && !found_text) {
gchar *linecopy = g_strdup (line);
g_ptr_array_add (builder, linecopy);
if (utf8_string_contains_alnum (linecopy)) {
GST_DEBUG ("Non-empty line '%s'", line);
found_text = TRUE;
}
} else {
g_ptr_array_add (builder, g_strdup (line));
}
}
out:
if (ret) {
gchar *newfile;
/* Add NULL-terminator to string list */
g_ptr_array_add (builder, NULL);
newfile = g_strjoinv ("\n", (gchar **) builder->pdata);
GST_DEBUG ("newfile:\n%s", newfile);
gst_buffer_unref (*buffer);
*buffer = gst_buffer_new_wrapped (newfile, strlen (newfile));
}
GST_DEBUG_OBJECT (hls_stream,
"Stream time %" GST_STIME_FORMAT " -> %" GST_STIME_FORMAT,
GST_STIME_ARGS (low_stream_time), GST_STIME_ARGS (high_stream_time));
g_ptr_array_unref (builder);
g_strfreev (original_lines);
g_free (original_content);
if (out_of_bounds) {
GstM3U8MediaSegment *candidate_segment;
/* The computed stream time falls outside of the guesstimated stream time,
* reassess which segment we really are in */
GST_WARNING ("Cue %" GST_STIME_FORMAT " -> %" GST_STIME_FORMAT
" is outside of segment %" GST_STIME_FORMAT " -> %"
GST_STIME_FORMAT, GST_STIME_ARGS (low_stream_time),
GST_STIME_ARGS (high_stream_time),
GST_STIME_ARGS (current_segment->stream_time),
GST_STIME_ARGS (current_segment->stream_time +
current_segment->duration));
candidate_segment =
gst_hls_media_playlist_seek (hls_stream->playlist, TRUE,
GST_SEEK_FLAG_SNAP_NEAREST, low_stream_time);
if (candidate_segment) {
g_assert (candidate_segment != current_segment);
GST_DEBUG_OBJECT (hls_stream,
"Stream time corresponds to segment %" GST_STIME_FORMAT
" duration %" GST_TIME_FORMAT,
GST_STIME_ARGS (candidate_segment->stream_time),
GST_TIME_ARGS (candidate_segment->duration));
/* Recalculate everything and ask parent class to restart */
hls_stream->current_segment->stream_time = candidate_segment->stream_time;
gst_hls_media_playlist_recalculate_stream_time (hls_stream->playlist,
hls_stream->current_segment);
gst_m3u8_media_segment_unref (candidate_segment);
}
}
if (!found_text) {
GST_DEBUG_OBJECT (hls_stream, "Replacing buffer with droppable buffer");
GST_BUFFER_PTS (*buffer) =
current_segment->stream_time + hls_stream->presentation_offset;
GST_BUFFER_DURATION (*buffer) = current_segment->duration;
gst_buffer_set_flags (*buffer, GST_BUFFER_FLAG_DROPPABLE);
}
return ret;
}
/* Get a utf8-validated string of the contents of the buffer */
gchar *
gst_hls_buf_to_utf8_text (GstBuffer * buf)
{
GstMapInfo info;
gchar *playlist;
if (!gst_buffer_map (buf, &info, GST_MAP_READ))
goto map_error;
if (!g_utf8_validate ((gchar *) info.data, info.size, NULL))
goto validate_error;
/* alloc size + 1 to end with a null character */
playlist = g_malloc0 (info.size + 1);
memcpy (playlist, info.data, info.size);
gst_buffer_unmap (buf, &info);
return playlist;
validate_error:
gst_buffer_unmap (buf, &info);
map_error:
return NULL;
}