gstreamer/ext/hls/gsthlsdemux-util.c
Sebastian Dröge ee647ca4be hlsdemux: Extract ID3 tags, strip them from the output and provide them as tags
They often don't only contain the PCR information but also other
metadata, like title. Give this information to the pipeline.

Also strip the tags from the stream as we a) already parsed them now and
b) decoders don't like these tags to happen in the middle of the stream
(i.e. the start of each fragment) and tagdemux only can strip them off
the beginning and end.
2016-12-19 11:51:36 +02:00

360 lines
10 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 <gst/gst.h>
#include <gst/tag/tag.h>
#include <string.h>
#include "gsthlsdemux.h"
GST_DEBUG_CATEGORY_EXTERN (gst_hls_demux_debug);
#define GST_CAT_DEFAULT gst_hls_demux_debug
/* 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;
}
static gint
find_offset (GstHLSTSReader * r, const guint8 * data, guint size)
{
guint sync_points = CLAMP (size / 188, 25, 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)) {
r->packet_size = packet_size;
return off;
}
}
return -1;
}
static gboolean
handle_pcr (GstHLSTSReader * r, const guint8 * data, guint size)
{
const guint8 *p = data;
guint32 hdr = GST_READ_UINT32_BE (p);
guint af_len, flags;
guint64 pcr_base, pcr_ext, pcr, ts;
data = p + 4;
if ((hdr & 0x00000020) == 0) /* has_adaptation_field */
return FALSE;
af_len = p[4]; /* adaptation_field_len */
++data;
if (af_len < (1 + 6) || af_len > r->packet_size - (4 + 1))
return FALSE;
flags = data[0];
/* Does the packet have a PCR? */
if ((flags & 0x10) == 0)
return FALSE;
++data;
--af_len;
pcr_base = (GST_READ_UINT64_BE (data) >> 16) >> (6 + 9);
pcr_ext = (GST_READ_UINT64_BE (data) >> 16) & 0x1ff;
pcr = pcr_base * 300 + pcr_ext;
ts = PCRTIME_TO_GSTTIME (pcr);
GST_LOG ("have PCR! %" G_GUINT64_FORMAT "\t%" GST_TIME_FORMAT,
pcr, GST_TIME_ARGS (ts));
if (r->first_pcr == GST_CLOCK_TIME_NONE)
r->first_pcr = ts;
r->last_pcr = ts;
return TRUE;
}
static gboolean
handle_pmt (GstHLSTSReader * r, const guint8 * data, guint size)
{
const guint8 *p = data;
guint32 hdr = GST_READ_UINT32_BE (p);
guint slen, pcr_pid;
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 + r->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 now: %04x", pcr_pid);
r->pcr_pid = pcr_pid;
return TRUE;
}
return FALSE;
}
static gboolean
handle_pat (GstHLSTSReader * r, const guint8 * data, guint size)
{
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 + r->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\n", program_num, val);
r->pmt_pid = val;
return TRUE;
}
data += 4;
slen -= 4;
}
return FALSE;
}
void
gst_hlsdemux_tsreader_init (GstHLSTSReader * r)
{
r->rtype = GST_HLS_TSREADER_NONE;
r->packet_size = 188;
r->pmt_pid = r->pcr_pid = -1;
r->first_pcr = GST_CLOCK_TIME_NONE;
r->last_pcr = GST_CLOCK_TIME_NONE;
}
void
gst_hlsdemux_tsreader_set_type (GstHLSTSReader * r, GstHLSTSReaderType rtype)
{
r->rtype = rtype;
r->have_id3 = FALSE;
}
static gboolean
gst_hlsdemux_tsreader_find_pcrs_mpegts (GstHLSTSReader * r,
GstBuffer * buffer, GstClockTime * first_pcr, GstClockTime * last_pcr)
{
GstMapInfo info;
gint offset;
const guint8 *p;
const guint8 *data;
gsize size;
if (!gst_buffer_map (buffer, &info, GST_MAP_READ))
return FALSE;
data = info.data;
size = info.size;
*first_pcr = *last_pcr = GST_CLOCK_TIME_NONE;
offset = find_offset (r, data, size);
if (offset < 0) {
gst_buffer_unmap (buffer, &info);
return FALSE;
}
GST_LOG ("TS packet start offset: %d", offset);
/* We don't store a partial packet at the end,
* and just assume that the final PCR is
* going to be completely inside the last data
* segment passed to us */
data += offset;
size -= offset;
for (p = data; size >= r->packet_size;
p += r->packet_size, size -= r->packet_size) {
guint32 hdr = GST_READ_UINT32_BE (p);
/* sync byte (0x47), error indicator (TEI) not set, PID 0, has_payload */
if ((hdr & 0xFF9FFF10) == 0x47000010) {
GST_LOG ("Found packet for PID 0000 (PAT)");
handle_pat (r, p, size);
}
/* sync byte (0x47), error indicator (TEI) not set, has_payload, PID = PMT_pid */
else if ((hdr & 0xFF800010) == 0x47000010
&& ((hdr >> 8) & 0x1fff) == r->pmt_pid) {
GST_LOG ("Found packet for PID %04x (PMT)", r->pmt_pid);
handle_pmt (r, p, size);
}
/* sync byte (0x47), error indicator (TEI) not set, has_payload */
else if ((hdr & 0xFF800010) == 0x47000010
&& ((hdr >> 8) & 0x1fff) == r->pcr_pid) {
GST_LOG ("Found packet for PID %04x (PCR)", r->pcr_pid);
handle_pcr (r, p, size);
}
}
gst_buffer_unmap (buffer, &info);
*first_pcr = r->first_pcr;
*last_pcr = r->last_pcr;
/* Return TRUE if this piece was big enough to get a PCR from */
return (r->first_pcr != GST_CLOCK_TIME_NONE);
}
static gboolean
gst_hlsdemux_tsreader_find_pcrs_id3 (GstHLSTSReader * r,
GstBuffer ** buffer_out, GstClockTime * first_pcr, GstClockTime * last_pcr,
GstTagList ** tags)
{
GstMapInfo info;
guint32 tag_size;
gsize size;
GstTagList *taglist;
GstSample *priv_data = NULL;
GstBuffer *buffer = *buffer_out;
GstBuffer *tag_buf;
guint64 pts;
*first_pcr = r->first_pcr;
*last_pcr = r->last_pcr;
if (r->have_id3)
return TRUE;
/* We need at least 10 bytes, starting with "ID3" for the header */
size = gst_buffer_get_size (buffer);
if (size < 10)
return FALSE;
/* Read the tag size */
tag_size = gst_tag_get_id3v2_tag_size (buffer);
/* Check we've collected that much */
if (size < tag_size)
return FALSE;
/* From here, whether the tag is valid or not we'll
* not try and read again */
r->have_id3 = TRUE;
*buffer_out =
gst_buffer_copy_region (buffer, GST_BUFFER_COPY_ALL, tag_size, -1);
/* Parse the tag */
taglist = gst_tag_list_from_id3v2_tag (buffer);
if (taglist == NULL) {
gst_buffer_unref (buffer);
return TRUE; /* Invalid tag, stop trying */
}
*tags = taglist;
/* 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;
pts = GST_READ_UINT64_BE (info.data);
*first_pcr = r->first_pcr = MPEGTIME_TO_GSTTIME (pts);
GST_LOG ("Got AAC TS PTS %" G_GUINT64_FORMAT " (%" G_GUINT64_FORMAT ")",
pts, r->first_pcr);
gst_buffer_unmap (tag_buf, &info);
out:
if (priv_data)
gst_sample_unref (priv_data);
gst_buffer_unref (buffer);
return TRUE;
}
gboolean
gst_hlsdemux_tsreader_find_pcrs (GstHLSTSReader * r,
GstBuffer ** buffer, GstClockTime * first_pcr, GstClockTime * last_pcr,
GstTagList ** tags)
{
*tags = NULL;
if (r->rtype == GST_HLS_TSREADER_MPEGTS)
return gst_hlsdemux_tsreader_find_pcrs_mpegts (r, *buffer, first_pcr,
last_pcr);
return gst_hlsdemux_tsreader_find_pcrs_id3 (r, buffer, first_pcr, last_pcr,
tags);
}