mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-03 16:09:39 +00:00
73721ad4e9
When a MSS server hosts a live stream the fragments listed in the manifest usually don't have accurate timestamps and duration, except for the first fragment, which additionally stores timing information for the few upcoming fragments. In this scenario it is useless to periodically fetch and update the manifest and the fragments list can be incrementally built by parsing the first/current fragment. https://bugzilla.gnome.org/show_bug.cgi?id=755036
1654 lines
45 KiB
C
1654 lines
45 KiB
C
/* GStreamer
|
|
* Copyright (C) 2012 Smart TV Alliance
|
|
* Copyright (C) 2016 Igalia S.L
|
|
* Copyright (C) 2016 Metrological
|
|
* Author: Thiago Sousa Santos <thiago.sousa.santos@collabora.com>, Collabora Ltd.
|
|
*
|
|
* gstmssmanifest.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., 59 Temple Place - Suite 330,
|
|
* Boston, MA 02111-1307, USA.
|
|
*/
|
|
|
|
#include <glib.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <ctype.h>
|
|
#include <libxml/parser.h>
|
|
#include <libxml/tree.h>
|
|
|
|
/* for parsing h264 codec data */
|
|
#include <gst/codecparsers/gsth264parser.h>
|
|
|
|
#include "gstmssmanifest.h"
|
|
#include "gstmssfragmentparser.h"
|
|
|
|
GST_DEBUG_CATEGORY_EXTERN (mssdemux_debug);
|
|
#define GST_CAT_DEFAULT mssdemux_debug
|
|
|
|
#define DEFAULT_TIMESCALE 10000000
|
|
|
|
#define MSS_NODE_STREAM_FRAGMENT "c"
|
|
#define MSS_NODE_STREAM_QUALITY "QualityLevel"
|
|
|
|
#define MSS_PROP_BITRATE "Bitrate"
|
|
#define MSS_PROP_DURATION "d"
|
|
#define MSS_PROP_DVR_WINDOW_LENGTH "DVRWindowLength"
|
|
#define MSS_PROP_LANGUAGE "Language"
|
|
#define MSS_PROP_NUMBER "n"
|
|
#define MSS_PROP_REPETITIONS "r"
|
|
#define MSS_PROP_STREAM_DURATION "Duration"
|
|
#define MSS_PROP_TIME "t"
|
|
#define MSS_PROP_TIMESCALE "TimeScale"
|
|
#define MSS_PROP_URL "Url"
|
|
|
|
typedef struct _GstMssStreamFragment
|
|
{
|
|
guint number;
|
|
guint64 time;
|
|
guint64 duration;
|
|
guint repetitions;
|
|
} GstMssStreamFragment;
|
|
|
|
typedef struct _GstMssStreamQuality
|
|
{
|
|
xmlNodePtr xmlnode;
|
|
|
|
gchar *bitrate_str;
|
|
guint64 bitrate;
|
|
} GstMssStreamQuality;
|
|
|
|
struct _GstMssStream
|
|
{
|
|
xmlNodePtr xmlnode;
|
|
|
|
gboolean active; /* if the stream is currently being used */
|
|
gint selectedQualityIndex;
|
|
|
|
gboolean has_live_fragments;
|
|
GstAdapter *live_adapter;
|
|
|
|
GList *fragments;
|
|
GList *qualities;
|
|
|
|
gchar *url;
|
|
gchar *lang;
|
|
|
|
GstMssFragmentParser fragment_parser;
|
|
|
|
guint fragment_repetition_index;
|
|
GList *current_fragment;
|
|
GList *current_quality;
|
|
|
|
/* TODO move this to somewhere static */
|
|
GRegex *regex_bitrate;
|
|
GRegex *regex_position;
|
|
};
|
|
|
|
struct _GstMssManifest
|
|
{
|
|
xmlDocPtr xml;
|
|
xmlNodePtr xmlrootnode;
|
|
|
|
gboolean is_live;
|
|
gint64 dvr_window;
|
|
guint64 look_ahead_fragment_count;
|
|
|
|
GString *protection_system_id;
|
|
gchar *protection_data;
|
|
|
|
GSList *streams;
|
|
};
|
|
|
|
/* For parsing and building a fragments list */
|
|
typedef struct _GstMssFragmentListBuilder
|
|
{
|
|
GList *fragments;
|
|
|
|
GstMssStreamFragment *previous_fragment;
|
|
guint fragment_number;
|
|
guint64 fragment_time_accum;
|
|
} GstMssFragmentListBuilder;
|
|
|
|
static void
|
|
gst_mss_fragment_list_builder_init (GstMssFragmentListBuilder * builder)
|
|
{
|
|
builder->fragments = NULL;
|
|
builder->previous_fragment = NULL;
|
|
builder->fragment_time_accum = 0;
|
|
builder->fragment_number = 0;
|
|
}
|
|
|
|
static void
|
|
gst_mss_fragment_list_builder_add (GstMssFragmentListBuilder * builder,
|
|
xmlNodePtr node)
|
|
{
|
|
gchar *duration_str;
|
|
gchar *time_str;
|
|
gchar *seqnum_str;
|
|
gchar *repetition_str;
|
|
GstMssStreamFragment *fragment = g_new (GstMssStreamFragment, 1);
|
|
|
|
duration_str = (gchar *) xmlGetProp (node, (xmlChar *) MSS_PROP_DURATION);
|
|
time_str = (gchar *) xmlGetProp (node, (xmlChar *) MSS_PROP_TIME);
|
|
seqnum_str = (gchar *) xmlGetProp (node, (xmlChar *) MSS_PROP_NUMBER);
|
|
repetition_str =
|
|
(gchar *) xmlGetProp (node, (xmlChar *) MSS_PROP_REPETITIONS);
|
|
|
|
/* use the node's seq number or use the previous + 1 */
|
|
if (seqnum_str) {
|
|
fragment->number = g_ascii_strtoull (seqnum_str, NULL, 10);
|
|
xmlFree (seqnum_str);
|
|
builder->fragment_number = fragment->number;
|
|
} else {
|
|
fragment->number = builder->fragment_number;
|
|
}
|
|
builder->fragment_number = fragment->number + 1;
|
|
|
|
if (repetition_str) {
|
|
fragment->repetitions = g_ascii_strtoull (repetition_str, NULL, 10);
|
|
xmlFree (repetition_str);
|
|
} else {
|
|
fragment->repetitions = 1;
|
|
}
|
|
|
|
if (time_str) {
|
|
fragment->time = g_ascii_strtoull (time_str, NULL, 10);
|
|
|
|
xmlFree (time_str);
|
|
builder->fragment_time_accum = fragment->time;
|
|
} else {
|
|
fragment->time = builder->fragment_time_accum;
|
|
}
|
|
|
|
/* if we have a previous fragment, means we need to set its duration */
|
|
if (builder->previous_fragment)
|
|
builder->previous_fragment->duration =
|
|
(fragment->time -
|
|
builder->previous_fragment->time) /
|
|
builder->previous_fragment->repetitions;
|
|
|
|
if (duration_str) {
|
|
fragment->duration = g_ascii_strtoull (duration_str, NULL, 10);
|
|
|
|
builder->previous_fragment = NULL;
|
|
builder->fragment_time_accum += fragment->duration * fragment->repetitions;
|
|
xmlFree (duration_str);
|
|
} else {
|
|
/* store to set the duration at the next iteration */
|
|
builder->previous_fragment = fragment;
|
|
}
|
|
|
|
/* we reverse it later */
|
|
builder->fragments = g_list_prepend (builder->fragments, fragment);
|
|
GST_LOG ("Adding fragment number: %u, time: %" G_GUINT64_FORMAT
|
|
", duration: %" G_GUINT64_FORMAT ", repetitions: %u",
|
|
fragment->number, fragment->time, fragment->duration,
|
|
fragment->repetitions);
|
|
}
|
|
|
|
static GstBuffer *gst_buffer_from_hex_string (const gchar * s);
|
|
|
|
static gboolean
|
|
node_has_type (xmlNodePtr node, const gchar * name)
|
|
{
|
|
return strcmp ((gchar *) node->name, name) == 0;
|
|
}
|
|
|
|
static GstMssStreamQuality *
|
|
gst_mss_stream_quality_new (xmlNodePtr node)
|
|
{
|
|
GstMssStreamQuality *q = g_slice_new (GstMssStreamQuality);
|
|
|
|
q->xmlnode = node;
|
|
q->bitrate_str = (gchar *) xmlGetProp (node, (xmlChar *) MSS_PROP_BITRATE);
|
|
|
|
if (q->bitrate_str != NULL)
|
|
q->bitrate = g_ascii_strtoull (q->bitrate_str, NULL, 10);
|
|
else
|
|
q->bitrate = 0;
|
|
|
|
return q;
|
|
}
|
|
|
|
static void
|
|
gst_mss_stream_quality_free (GstMssStreamQuality * quality)
|
|
{
|
|
g_return_if_fail (quality != NULL);
|
|
|
|
xmlFree (quality->bitrate_str);
|
|
g_slice_free (GstMssStreamQuality, quality);
|
|
}
|
|
|
|
static gint
|
|
compare_bitrate (GstMssStreamQuality * a, GstMssStreamQuality * b)
|
|
{
|
|
if (a->bitrate > b->bitrate)
|
|
return 1;
|
|
if (a->bitrate < b->bitrate)
|
|
return -1;
|
|
return 0;
|
|
|
|
}
|
|
|
|
static void
|
|
_gst_mss_stream_init (GstMssManifest * manifest, GstMssStream * stream,
|
|
xmlNodePtr node)
|
|
{
|
|
xmlNodePtr iter;
|
|
GstMssFragmentListBuilder builder;
|
|
|
|
gst_mss_fragment_list_builder_init (&builder);
|
|
|
|
stream->xmlnode = node;
|
|
|
|
/* get the base url path generator */
|
|
stream->url = (gchar *) xmlGetProp (node, (xmlChar *) MSS_PROP_URL);
|
|
stream->lang = (gchar *) xmlGetProp (node, (xmlChar *) MSS_PROP_LANGUAGE);
|
|
|
|
/* for live playback each fragment usually has timing
|
|
* information for the few next look-ahead fragments so the
|
|
* playlist can be built incrementally from the first fragment
|
|
* of the manifest.
|
|
*/
|
|
|
|
GST_DEBUG ("Live stream: %s, look-ahead fragments: %" G_GUINT64_FORMAT,
|
|
manifest->is_live ? "yes" : "no", manifest->look_ahead_fragment_count);
|
|
stream->has_live_fragments = manifest->is_live
|
|
&& manifest->look_ahead_fragment_count;
|
|
|
|
for (iter = node->children; iter; iter = iter->next) {
|
|
if (node_has_type (iter, MSS_NODE_STREAM_FRAGMENT)) {
|
|
if (!stream->has_live_fragments || !builder.fragments)
|
|
gst_mss_fragment_list_builder_add (&builder, iter);
|
|
} else if (node_has_type (iter, MSS_NODE_STREAM_QUALITY)) {
|
|
GstMssStreamQuality *quality = gst_mss_stream_quality_new (iter);
|
|
stream->qualities = g_list_prepend (stream->qualities, quality);
|
|
} else {
|
|
/* TODO gst log this */
|
|
}
|
|
}
|
|
|
|
if (stream->has_live_fragments) {
|
|
stream->live_adapter = gst_adapter_new ();
|
|
}
|
|
|
|
if (builder.fragments) {
|
|
stream->fragments = g_list_reverse (builder.fragments);
|
|
stream->current_fragment = stream->fragments;
|
|
}
|
|
|
|
/* order them from smaller to bigger based on bitrates */
|
|
stream->qualities =
|
|
g_list_sort (stream->qualities, (GCompareFunc) compare_bitrate);
|
|
stream->current_quality = stream->qualities;
|
|
|
|
stream->regex_bitrate = g_regex_new ("\\{[Bb]itrate\\}", 0, 0, NULL);
|
|
stream->regex_position = g_regex_new ("\\{start[ _]time\\}", 0, 0, NULL);
|
|
|
|
gst_mss_fragment_parser_init (&stream->fragment_parser);
|
|
}
|
|
|
|
|
|
static void
|
|
_gst_mss_parse_protection (GstMssManifest * manifest,
|
|
xmlNodePtr protection_node)
|
|
{
|
|
xmlNodePtr nodeiter;
|
|
|
|
for (nodeiter = protection_node->children; nodeiter;
|
|
nodeiter = nodeiter->next) {
|
|
if (nodeiter->type == XML_ELEMENT_NODE
|
|
&& (strcmp ((const char *) nodeiter->name, "ProtectionHeader") == 0)) {
|
|
xmlChar *system_id_attribute =
|
|
xmlGetProp (nodeiter, (xmlChar *) "SystemID");
|
|
gchar *value = (gchar *) system_id_attribute;
|
|
int id_len = strlen (value);
|
|
GString *system_id;
|
|
|
|
if (value[0] == '{') {
|
|
value++;
|
|
id_len--;
|
|
}
|
|
|
|
system_id = g_string_new (value);
|
|
system_id = g_string_ascii_down (system_id);
|
|
if (value[id_len - 1] == '}')
|
|
system_id = g_string_truncate (system_id, id_len - 1);
|
|
|
|
manifest->protection_system_id = system_id;
|
|
manifest->protection_data = (gchar *) xmlNodeGetContent (nodeiter);
|
|
xmlFree (system_id_attribute);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
GstMssManifest *
|
|
gst_mss_manifest_new (GstBuffer * data)
|
|
{
|
|
GstMssManifest *manifest;
|
|
xmlNodePtr root;
|
|
xmlNodePtr nodeiter;
|
|
gchar *live_str;
|
|
GstMapInfo mapinfo;
|
|
gchar *look_ahead_fragment_count_str;
|
|
|
|
if (!gst_buffer_map (data, &mapinfo, GST_MAP_READ)) {
|
|
return NULL;
|
|
}
|
|
|
|
manifest = g_malloc0 (sizeof (GstMssManifest));
|
|
|
|
manifest->xml = xmlReadMemory ((const gchar *) mapinfo.data,
|
|
mapinfo.size, "manifest", NULL, 0);
|
|
root = manifest->xmlrootnode = xmlDocGetRootElement (manifest->xml);
|
|
|
|
live_str = (gchar *) xmlGetProp (root, (xmlChar *) "IsLive");
|
|
if (live_str) {
|
|
manifest->is_live = g_ascii_strcasecmp (live_str, "true") == 0;
|
|
xmlFree (live_str);
|
|
}
|
|
|
|
/* the entire file is always available for non-live streams */
|
|
if (!manifest->is_live) {
|
|
manifest->dvr_window = 0;
|
|
manifest->look_ahead_fragment_count = 0;
|
|
} else {
|
|
/* if 0, or non-existent, the length is infinite */
|
|
gchar *dvr_window_str = (gchar *) xmlGetProp (root,
|
|
(xmlChar *) MSS_PROP_DVR_WINDOW_LENGTH);
|
|
if (dvr_window_str) {
|
|
manifest->dvr_window = g_ascii_strtoull (dvr_window_str, NULL, 10);
|
|
xmlFree (dvr_window_str);
|
|
if (manifest->dvr_window <= 0) {
|
|
manifest->dvr_window = 0;
|
|
}
|
|
}
|
|
|
|
look_ahead_fragment_count_str =
|
|
(gchar *) xmlGetProp (root, (xmlChar *) "LookAheadFragmentCount");
|
|
if (look_ahead_fragment_count_str) {
|
|
manifest->look_ahead_fragment_count =
|
|
g_ascii_strtoull (look_ahead_fragment_count_str, NULL, 10);
|
|
xmlFree (look_ahead_fragment_count_str);
|
|
if (manifest->look_ahead_fragment_count <= 0) {
|
|
manifest->look_ahead_fragment_count = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (nodeiter = root->children; nodeiter; nodeiter = nodeiter->next) {
|
|
if (nodeiter->type == XML_ELEMENT_NODE
|
|
&& (strcmp ((const char *) nodeiter->name, "StreamIndex") == 0)) {
|
|
GstMssStream *stream = g_new0 (GstMssStream, 1);
|
|
|
|
manifest->streams = g_slist_append (manifest->streams, stream);
|
|
_gst_mss_stream_init (manifest, stream, nodeiter);
|
|
}
|
|
|
|
if (nodeiter->type == XML_ELEMENT_NODE
|
|
&& (strcmp ((const char *) nodeiter->name, "Protection") == 0)) {
|
|
_gst_mss_parse_protection (manifest, nodeiter);
|
|
}
|
|
}
|
|
|
|
gst_buffer_unmap (data, &mapinfo);
|
|
|
|
return manifest;
|
|
}
|
|
|
|
static void
|
|
gst_mss_stream_free (GstMssStream * stream)
|
|
{
|
|
if (stream->live_adapter) {
|
|
gst_adapter_clear (stream->live_adapter);
|
|
g_object_unref (stream->live_adapter);
|
|
}
|
|
|
|
g_list_free_full (stream->fragments, g_free);
|
|
g_list_free_full (stream->qualities,
|
|
(GDestroyNotify) gst_mss_stream_quality_free);
|
|
xmlFree (stream->url);
|
|
xmlFree (stream->lang);
|
|
g_regex_unref (stream->regex_position);
|
|
g_regex_unref (stream->regex_bitrate);
|
|
g_free (stream);
|
|
gst_mss_fragment_parser_clear (&stream->fragment_parser);
|
|
}
|
|
|
|
void
|
|
gst_mss_manifest_free (GstMssManifest * manifest)
|
|
{
|
|
g_return_if_fail (manifest != NULL);
|
|
|
|
g_slist_free_full (manifest->streams, (GDestroyNotify) gst_mss_stream_free);
|
|
|
|
if (manifest->protection_system_id != NULL)
|
|
g_string_free (manifest->protection_system_id, TRUE);
|
|
xmlFree (manifest->protection_data);
|
|
|
|
xmlFreeDoc (manifest->xml);
|
|
g_free (manifest);
|
|
}
|
|
|
|
const gchar *
|
|
gst_mss_manifest_get_protection_system_id (GstMssManifest * manifest)
|
|
{
|
|
if (manifest->protection_system_id != NULL)
|
|
return manifest->protection_system_id->str;
|
|
return NULL;
|
|
}
|
|
|
|
const gchar *
|
|
gst_mss_manifest_get_protection_data (GstMssManifest * manifest)
|
|
{
|
|
return manifest->protection_data;
|
|
}
|
|
|
|
GSList *
|
|
gst_mss_manifest_get_streams (GstMssManifest * manifest)
|
|
{
|
|
return manifest->streams;
|
|
}
|
|
|
|
GstMssStreamType
|
|
gst_mss_stream_get_type (GstMssStream * stream)
|
|
{
|
|
gchar *prop = (gchar *) xmlGetProp (stream->xmlnode, (xmlChar *) "Type");
|
|
GstMssStreamType ret = MSS_STREAM_TYPE_UNKNOWN;
|
|
|
|
if (prop == NULL)
|
|
return MSS_STREAM_TYPE_UNKNOWN;
|
|
|
|
if (strcmp (prop, "video") == 0) {
|
|
ret = MSS_STREAM_TYPE_VIDEO;
|
|
} else if (strcmp (prop, "audio") == 0) {
|
|
ret = MSS_STREAM_TYPE_AUDIO;
|
|
} else {
|
|
GST_DEBUG ("Unsupported stream type: %s", prop);
|
|
}
|
|
xmlFree (prop);
|
|
return ret;
|
|
}
|
|
|
|
static GstCaps *
|
|
_gst_mss_stream_video_caps_from_fourcc (gchar * fourcc)
|
|
{
|
|
if (!fourcc)
|
|
return NULL;
|
|
|
|
if (strcmp (fourcc, "H264") == 0 || strcmp (fourcc, "AVC1") == 0) {
|
|
return gst_caps_new_simple ("video/x-h264", "stream-format", G_TYPE_STRING,
|
|
"avc", NULL);
|
|
} else if (strcmp (fourcc, "WVC1") == 0) {
|
|
return gst_caps_new_simple ("video/x-wmv", "wmvversion", G_TYPE_INT, 3,
|
|
"format", G_TYPE_STRING, "WVC1", NULL);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static GstCaps *
|
|
_gst_mss_stream_audio_caps_from_fourcc (gchar * fourcc)
|
|
{
|
|
if (!fourcc)
|
|
return NULL;
|
|
|
|
if (strcmp (fourcc, "AACL") == 0) {
|
|
return gst_caps_new_simple ("audio/mpeg", "mpegversion", G_TYPE_INT, 4,
|
|
NULL);
|
|
} else if (strcmp (fourcc, "WmaPro") == 0 || strcmp (fourcc, "WMAP") == 0) {
|
|
return gst_caps_new_simple ("audio/x-wma", "wmaversion", G_TYPE_INT, 3,
|
|
NULL);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static GstCaps *
|
|
_gst_mss_stream_audio_caps_from_audio_tag (gint audiotag)
|
|
{
|
|
switch (audiotag) {
|
|
case 83: /* MP3 */
|
|
return gst_caps_new_simple ("audio/mpeg", "mpegversion", G_TYPE_INT, 1,
|
|
"layer", G_TYPE_INT, 3, NULL);
|
|
case 255: /* AAC */
|
|
return gst_caps_new_simple ("audio/mpeg", "mpegversion", G_TYPE_INT, 4,
|
|
NULL);
|
|
default:
|
|
break;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* copied and adapted from h264parse */
|
|
static GstBuffer *
|
|
_make_h264_codec_data (GstBuffer * sps, GstBuffer * pps)
|
|
{
|
|
GstBuffer *buf;
|
|
gint sps_size = 0, pps_size = 0, num_sps = 0, num_pps = 0;
|
|
guint8 profile_idc = 0, profile_comp = 0, level_idc = 0;
|
|
guint8 *data;
|
|
gint nl;
|
|
GstMapInfo spsinfo, ppsinfo, codecdatainfo;
|
|
|
|
if (gst_buffer_get_size (sps) < 4)
|
|
return NULL;
|
|
|
|
gst_buffer_map (sps, &spsinfo, GST_MAP_READ);
|
|
gst_buffer_map (pps, &ppsinfo, GST_MAP_READ);
|
|
|
|
sps_size += spsinfo.size + 2;
|
|
profile_idc = spsinfo.data[1];
|
|
profile_comp = spsinfo.data[2];
|
|
level_idc = spsinfo.data[3];
|
|
num_sps = 1;
|
|
|
|
pps_size += ppsinfo.size + 2;
|
|
num_pps = 1;
|
|
|
|
buf = gst_buffer_new_and_alloc (5 + 1 + sps_size + 1 + pps_size);
|
|
gst_buffer_map (buf, &codecdatainfo, GST_MAP_WRITE);
|
|
data = codecdatainfo.data;
|
|
nl = 4;
|
|
|
|
data[0] = 1; /* AVC Decoder Configuration Record ver. 1 */
|
|
data[1] = profile_idc; /* profile_idc */
|
|
data[2] = profile_comp; /* profile_compability */
|
|
data[3] = level_idc; /* level_idc */
|
|
data[4] = 0xfc | (nl - 1); /* nal_length_size_minus1 */
|
|
data[5] = 0xe0 | num_sps; /* number of SPSs */
|
|
|
|
data += 6;
|
|
GST_WRITE_UINT16_BE (data, spsinfo.size);
|
|
memcpy (data + 2, spsinfo.data, spsinfo.size);
|
|
data += 2 + spsinfo.size;
|
|
|
|
data[0] = num_pps;
|
|
data++;
|
|
GST_WRITE_UINT16_BE (data, ppsinfo.size);
|
|
memcpy (data + 2, ppsinfo.data, ppsinfo.size);
|
|
data += 2 + ppsinfo.size;
|
|
|
|
gst_buffer_unmap (sps, &spsinfo);
|
|
gst_buffer_unmap (pps, &ppsinfo);
|
|
gst_buffer_unmap (buf, &codecdatainfo);
|
|
|
|
return buf;
|
|
}
|
|
|
|
static void
|
|
_gst_mss_stream_add_h264_codec_data (GstCaps * caps, const gchar * codecdatastr)
|
|
{
|
|
GstBuffer *sps;
|
|
GstBuffer *pps;
|
|
GstBuffer *buffer;
|
|
gchar *sps_str;
|
|
gchar *pps_str;
|
|
GstH264NalUnit nalu = { 0, };
|
|
GstH264SPS sps_struct;
|
|
GstH264ParserResult parseres;
|
|
GstMapInfo spsinfo;
|
|
|
|
/* search for the sps start */
|
|
if (g_str_has_prefix (codecdatastr, "00000001")) {
|
|
sps_str = (gchar *) codecdatastr + 8;
|
|
} else {
|
|
return; /* invalid mss codec data */
|
|
}
|
|
|
|
/* search for the pps start */
|
|
pps_str = g_strstr_len (sps_str, -1, "00000001");
|
|
if (!pps_str) {
|
|
return; /* invalid mss codec data */
|
|
}
|
|
|
|
pps_str[0] = '\0';
|
|
sps = gst_buffer_from_hex_string (sps_str);
|
|
pps_str[0] = '0';
|
|
|
|
pps_str = pps_str + 8;
|
|
pps = gst_buffer_from_hex_string (pps_str);
|
|
|
|
gst_buffer_map (sps, &spsinfo, GST_MAP_READ);
|
|
|
|
nalu.ref_idc = (spsinfo.data[0] & 0x60) >> 5;
|
|
nalu.type = GST_H264_NAL_SPS;
|
|
nalu.size = spsinfo.size;
|
|
nalu.data = spsinfo.data;
|
|
nalu.offset = 0;
|
|
nalu.sc_offset = 0;
|
|
nalu.valid = TRUE;
|
|
nalu.header_bytes = 0;
|
|
nalu.extension_type = GST_H264_NAL_EXTENSION_NONE;
|
|
|
|
parseres = gst_h264_parse_sps (&nalu, &sps_struct, TRUE);
|
|
if (parseres == GST_H264_PARSER_OK) {
|
|
gint fps_num, fps_den;
|
|
|
|
/* MSS apparently only supports non-interlaced/progressive H.264 content */
|
|
gst_h264_video_calculate_framerate (&sps_struct, 0, 0, &fps_num, &fps_den);
|
|
|
|
gst_caps_set_simple (caps, "framerate", GST_TYPE_FRACTION,
|
|
fps_num, fps_den, NULL);
|
|
}
|
|
|
|
buffer = _make_h264_codec_data (sps, pps);
|
|
gst_buffer_unmap (sps, &spsinfo);
|
|
gst_buffer_unref (sps);
|
|
gst_buffer_unref (pps);
|
|
|
|
if (buffer != NULL) {
|
|
gst_caps_set_simple (caps, "codec_data", GST_TYPE_BUFFER, buffer, NULL);
|
|
gst_buffer_unref (buffer);
|
|
}
|
|
}
|
|
|
|
static GstCaps *
|
|
_gst_mss_stream_video_caps_from_qualitylevel_xml (GstMssStreamQuality * q)
|
|
{
|
|
xmlNodePtr node = q->xmlnode;
|
|
GstCaps *caps;
|
|
GstStructure *structure;
|
|
gchar *fourcc = (gchar *) xmlGetProp (node, (xmlChar *) "FourCC");
|
|
gchar *max_width = (gchar *) xmlGetProp (node, (xmlChar *) "MaxWidth");
|
|
gchar *max_height = (gchar *) xmlGetProp (node, (xmlChar *) "MaxHeight");
|
|
gchar *codec_data =
|
|
(gchar *) xmlGetProp (node, (xmlChar *) "CodecPrivateData");
|
|
|
|
if (!max_width)
|
|
max_width = (gchar *) xmlGetProp (node, (xmlChar *) "Width");
|
|
if (!max_height)
|
|
max_height = (gchar *) xmlGetProp (node, (xmlChar *) "Height");
|
|
|
|
caps = _gst_mss_stream_video_caps_from_fourcc (fourcc);
|
|
if (!caps)
|
|
goto end;
|
|
|
|
structure = gst_caps_get_structure (caps, 0);
|
|
|
|
if (max_width) {
|
|
gst_structure_set (structure, "width", G_TYPE_INT,
|
|
(int) g_ascii_strtoull (max_width, NULL, 10), NULL);
|
|
}
|
|
if (max_height) {
|
|
gst_structure_set (structure, "height", G_TYPE_INT,
|
|
(int) g_ascii_strtoull (max_height, NULL, 10), NULL);
|
|
}
|
|
|
|
if (codec_data && strlen (codec_data)) {
|
|
if (strcmp (fourcc, "H264") == 0 || strcmp (fourcc, "AVC1") == 0) {
|
|
_gst_mss_stream_add_h264_codec_data (caps, codec_data);
|
|
} else {
|
|
GstBuffer *buffer = gst_buffer_from_hex_string ((gchar *) codec_data);
|
|
gst_structure_set (structure, "codec_data", GST_TYPE_BUFFER, buffer,
|
|
NULL);
|
|
gst_buffer_unref (buffer);
|
|
}
|
|
}
|
|
|
|
end:
|
|
xmlFree (fourcc);
|
|
xmlFree (max_width);
|
|
xmlFree (max_height);
|
|
xmlFree (codec_data);
|
|
|
|
return caps;
|
|
}
|
|
|
|
static guint8
|
|
_frequency_index_from_sampling_rate (guint sampling_rate)
|
|
{
|
|
static const guint aac_sample_rates[] = { 96000, 88200, 64000, 48000, 44100,
|
|
32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350
|
|
};
|
|
|
|
guint8 i;
|
|
|
|
for (i = 0; i < G_N_ELEMENTS (aac_sample_rates); i++) {
|
|
if (aac_sample_rates[i] == sampling_rate)
|
|
return i;
|
|
}
|
|
return 15;
|
|
}
|
|
|
|
static GstBuffer *
|
|
_make_aacl_codec_data (guint64 sampling_rate, guint64 channels)
|
|
{
|
|
GstBuffer *buf;
|
|
guint8 *data;
|
|
guint8 frequency_index;
|
|
guint8 buf_size;
|
|
GstMapInfo info;
|
|
|
|
buf_size = 2;
|
|
frequency_index = _frequency_index_from_sampling_rate (sampling_rate);
|
|
if (frequency_index == 15)
|
|
buf_size += 3;
|
|
|
|
buf = gst_buffer_new_and_alloc (buf_size);
|
|
gst_buffer_map (buf, &info, GST_MAP_WRITE);
|
|
data = info.data;
|
|
|
|
data[0] = 2 << 3; /* AAC-LC object type is 2 */
|
|
data[0] += frequency_index >> 1;
|
|
data[1] = (frequency_index & 0x01) << 7;
|
|
|
|
/* Sampling rate is not in frequencies table, write manually */
|
|
if (frequency_index == 15) {
|
|
data[1] += sampling_rate >> 17;
|
|
data[2] = (sampling_rate >> 9) & 0xFF;
|
|
data[3] = (sampling_rate >> 1) & 0xFF;
|
|
data[4] = sampling_rate & 0x01;
|
|
data += 3;
|
|
}
|
|
|
|
data[1] += (channels & 0x0F) << 3;
|
|
|
|
gst_buffer_unmap (buf, &info);
|
|
|
|
return buf;
|
|
}
|
|
|
|
static GstCaps *
|
|
_gst_mss_stream_audio_caps_from_qualitylevel_xml (GstMssStreamQuality * q)
|
|
{
|
|
xmlNodePtr node = q->xmlnode;
|
|
GstCaps *caps = NULL;
|
|
GstStructure *structure;
|
|
gchar *fourcc = (gchar *) xmlGetProp (node, (xmlChar *) "FourCC");
|
|
gchar *audiotag = (gchar *) xmlGetProp (node, (xmlChar *) "AudioTag");
|
|
gchar *channels_str = (gchar *) xmlGetProp (node, (xmlChar *) "Channels");
|
|
gchar *rate_str = (gchar *) xmlGetProp (node, (xmlChar *) "SamplingRate");
|
|
gchar *depth_str = (gchar *) xmlGetProp (node, (xmlChar *) "BitsPerSample");
|
|
gchar *block_align_str =
|
|
(gchar *) xmlGetProp (node, (xmlChar *) "PacketSize");
|
|
gchar *codec_data_str =
|
|
(gchar *) xmlGetProp (node, (xmlChar *) "CodecPrivateData");
|
|
GstBuffer *codec_data = NULL;
|
|
gint depth = 0;
|
|
gint block_align = 0;
|
|
gint rate = 0;
|
|
gint channels = 0;
|
|
gint atag = 0;
|
|
|
|
if (!fourcc) /* sometimes the fourcc is omitted, we fallback to the Subtype in the StreamIndex node */
|
|
fourcc = (gchar *) xmlGetProp (node->parent, (xmlChar *) "Subtype");
|
|
|
|
if (fourcc) {
|
|
caps = _gst_mss_stream_audio_caps_from_fourcc (fourcc);
|
|
} else if (audiotag) {
|
|
atag = g_ascii_strtoull (audiotag, NULL, 10);
|
|
caps = _gst_mss_stream_audio_caps_from_audio_tag (atag);
|
|
}
|
|
|
|
if (!caps)
|
|
goto end;
|
|
|
|
structure = gst_caps_get_structure (caps, 0);
|
|
if (codec_data_str && strlen (codec_data_str)) {
|
|
codec_data = gst_buffer_from_hex_string ((gchar *) codec_data_str);
|
|
}
|
|
|
|
if (rate_str)
|
|
rate = (gint) g_ascii_strtoull (rate_str, NULL, 10);
|
|
if (channels_str)
|
|
channels = (int) g_ascii_strtoull (channels_str, NULL, 10);
|
|
if (depth_str)
|
|
depth = (gint) g_ascii_strtoull (depth_str, NULL, 10);
|
|
if (block_align_str)
|
|
block_align = (int) g_ascii_strtoull (block_align_str, NULL, 10);
|
|
|
|
if (!codec_data) {
|
|
gint codec_data_len;
|
|
codec_data_str = (gchar *) xmlGetProp (node, (xmlChar *) "WaveFormatEx");
|
|
|
|
if (codec_data_str != NULL) {
|
|
codec_data_len = strlen (codec_data_str) / 2;
|
|
|
|
/* a WAVEFORMATEX structure is 18 bytes */
|
|
if (codec_data_str && codec_data_len >= 18) {
|
|
GstMapInfo mapinfo;
|
|
codec_data = gst_buffer_from_hex_string ((gchar *) codec_data_str);
|
|
|
|
/* since this is a WAVEFORMATEX, try to get the block_align and rate */
|
|
gst_buffer_map (codec_data, &mapinfo, GST_MAP_READ);
|
|
if (!channels_str) {
|
|
channels = GST_READ_UINT16_LE (mapinfo.data + 2);
|
|
}
|
|
if (!rate_str) {
|
|
rate = GST_READ_UINT32_LE (mapinfo.data + 4);
|
|
}
|
|
if (!block_align) {
|
|
block_align = GST_READ_UINT16_LE (mapinfo.data + 12);
|
|
}
|
|
if (!depth) {
|
|
depth = GST_READ_UINT16_LE (mapinfo.data + 14);
|
|
}
|
|
gst_buffer_unmap (codec_data, &mapinfo);
|
|
|
|
/* Consume all the WAVEFORMATEX structure, and pass only the rest of
|
|
* the data as the codec private data */
|
|
gst_buffer_resize (codec_data, 18, -1);
|
|
} else {
|
|
GST_WARNING ("Dropping WaveFormatEx: data is %d bytes, "
|
|
"but at least 18 bytes are expected", codec_data_len);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!codec_data && ((fourcc && strcmp (fourcc, "AACL") == 0) || atag == 255)
|
|
&& rate && channels) {
|
|
codec_data = _make_aacl_codec_data (rate, channels);
|
|
}
|
|
|
|
if (block_align)
|
|
gst_structure_set (structure, "block_align", G_TYPE_INT, block_align, NULL);
|
|
|
|
if (channels)
|
|
gst_structure_set (structure, "channels", G_TYPE_INT, channels, NULL);
|
|
|
|
if (rate)
|
|
gst_structure_set (structure, "rate", G_TYPE_INT, rate, NULL);
|
|
|
|
if (depth)
|
|
gst_structure_set (structure, "depth", G_TYPE_INT, depth, NULL);
|
|
|
|
if (q->bitrate)
|
|
gst_structure_set (structure, "bitrate", G_TYPE_INT, (int) q->bitrate,
|
|
NULL);
|
|
|
|
if (codec_data)
|
|
gst_structure_set (structure, "codec_data", GST_TYPE_BUFFER, codec_data,
|
|
NULL);
|
|
|
|
end:
|
|
if (codec_data)
|
|
gst_buffer_unref (codec_data);
|
|
xmlFree (fourcc);
|
|
xmlFree (audiotag);
|
|
xmlFree (channels_str);
|
|
xmlFree (rate_str);
|
|
xmlFree (depth_str);
|
|
xmlFree (block_align_str);
|
|
xmlFree (codec_data_str);
|
|
|
|
return caps;
|
|
}
|
|
|
|
void
|
|
gst_mss_stream_set_active (GstMssStream * stream, gboolean active)
|
|
{
|
|
stream->active = active;
|
|
}
|
|
|
|
guint64
|
|
gst_mss_stream_get_timescale (GstMssStream * stream)
|
|
{
|
|
gchar *timescale;
|
|
guint64 ts = DEFAULT_TIMESCALE;
|
|
|
|
timescale =
|
|
(gchar *) xmlGetProp (stream->xmlnode, (xmlChar *) MSS_PROP_TIMESCALE);
|
|
if (!timescale) {
|
|
timescale =
|
|
(gchar *) xmlGetProp (stream->xmlnode->parent,
|
|
(xmlChar *) MSS_PROP_TIMESCALE);
|
|
}
|
|
|
|
if (timescale) {
|
|
ts = g_ascii_strtoull (timescale, NULL, 10);
|
|
xmlFree (timescale);
|
|
}
|
|
return ts;
|
|
}
|
|
|
|
guint64
|
|
gst_mss_manifest_get_timescale (GstMssManifest * manifest)
|
|
{
|
|
gchar *timescale;
|
|
guint64 ts = DEFAULT_TIMESCALE;
|
|
|
|
timescale =
|
|
(gchar *) xmlGetProp (manifest->xmlrootnode,
|
|
(xmlChar *) MSS_PROP_TIMESCALE);
|
|
if (timescale) {
|
|
ts = g_ascii_strtoull (timescale, NULL, 10);
|
|
xmlFree (timescale);
|
|
}
|
|
return ts;
|
|
}
|
|
|
|
guint64
|
|
gst_mss_manifest_get_duration (GstMssManifest * manifest)
|
|
{
|
|
gchar *duration;
|
|
guint64 dur = -1;
|
|
|
|
/* try the property */
|
|
duration =
|
|
(gchar *) xmlGetProp (manifest->xmlrootnode,
|
|
(xmlChar *) MSS_PROP_STREAM_DURATION);
|
|
if (duration) {
|
|
dur = g_ascii_strtoull (duration, NULL, 10);
|
|
xmlFree (duration);
|
|
}
|
|
/* else use the fragment list */
|
|
if (dur <= 0) {
|
|
guint64 max_dur = 0;
|
|
GSList *iter;
|
|
|
|
for (iter = manifest->streams; iter; iter = g_slist_next (iter)) {
|
|
GstMssStream *stream = iter->data;
|
|
|
|
if (stream->active) {
|
|
if (stream->fragments) {
|
|
GList *l = g_list_last (stream->fragments);
|
|
GstMssStreamFragment *fragment = (GstMssStreamFragment *) l->data;
|
|
guint64 frag_dur =
|
|
fragment->time + fragment->duration * fragment->repetitions;
|
|
max_dur = MAX (frag_dur, max_dur);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (max_dur != 0)
|
|
dur = max_dur;
|
|
}
|
|
|
|
return dur;
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets the duration in nanoseconds
|
|
*/
|
|
GstClockTime
|
|
gst_mss_manifest_get_gst_duration (GstMssManifest * manifest)
|
|
{
|
|
guint64 duration = -1;
|
|
guint64 timescale;
|
|
GstClockTime gstdur = GST_CLOCK_TIME_NONE;
|
|
|
|
duration = gst_mss_manifest_get_duration (manifest);
|
|
timescale = gst_mss_manifest_get_timescale (manifest);
|
|
|
|
if (duration != -1 && timescale != -1)
|
|
gstdur =
|
|
(GstClockTime) gst_util_uint64_scale_round (duration, GST_SECOND,
|
|
timescale);
|
|
|
|
return gstdur;
|
|
}
|
|
|
|
GstClockTime
|
|
gst_mss_manifest_get_min_fragment_duration (GstMssManifest * manifest)
|
|
{
|
|
GSList *iter;
|
|
GstClockTime dur = GST_CLOCK_TIME_NONE;
|
|
GstClockTime iter_dur;
|
|
|
|
for (iter = manifest->streams; iter; iter = g_slist_next (iter)) {
|
|
GstMssStream *stream = iter->data;
|
|
|
|
iter_dur = gst_mss_stream_get_fragment_gst_duration (stream);
|
|
if (iter_dur != GST_CLOCK_TIME_NONE && iter_dur != 0) {
|
|
if (GST_CLOCK_TIME_IS_VALID (dur)) {
|
|
dur = MIN (dur, iter_dur);
|
|
} else {
|
|
dur = iter_dur;
|
|
}
|
|
}
|
|
}
|
|
|
|
return dur;
|
|
}
|
|
|
|
GstCaps *
|
|
gst_mss_stream_get_caps (GstMssStream * stream)
|
|
{
|
|
GstMssStreamType streamtype = gst_mss_stream_get_type (stream);
|
|
GstMssStreamQuality *qualitylevel = stream->current_quality->data;
|
|
GstCaps *caps = NULL;
|
|
|
|
if (streamtype == MSS_STREAM_TYPE_VIDEO)
|
|
caps = _gst_mss_stream_video_caps_from_qualitylevel_xml (qualitylevel);
|
|
else if (streamtype == MSS_STREAM_TYPE_AUDIO)
|
|
caps = _gst_mss_stream_audio_caps_from_qualitylevel_xml (qualitylevel);
|
|
|
|
return caps;
|
|
}
|
|
|
|
GstFlowReturn
|
|
gst_mss_stream_get_fragment_url (GstMssStream * stream, gchar ** url)
|
|
{
|
|
gchar *tmp;
|
|
gchar *start_time_str;
|
|
guint64 time;
|
|
GstMssStreamFragment *fragment;
|
|
GstMssStreamQuality *quality = stream->current_quality->data;
|
|
|
|
g_return_val_if_fail (stream->active, GST_FLOW_ERROR);
|
|
|
|
if (stream->current_fragment == NULL) /* stream is over */
|
|
return GST_FLOW_EOS;
|
|
|
|
fragment = stream->current_fragment->data;
|
|
|
|
time =
|
|
fragment->time + fragment->duration * stream->fragment_repetition_index;
|
|
start_time_str = g_strdup_printf ("%" G_GUINT64_FORMAT, time);
|
|
|
|
tmp = g_regex_replace_literal (stream->regex_bitrate, stream->url,
|
|
strlen (stream->url), 0, quality->bitrate_str, 0, NULL);
|
|
*url = g_regex_replace_literal (stream->regex_position, tmp,
|
|
strlen (tmp), 0, start_time_str, 0, NULL);
|
|
|
|
g_free (tmp);
|
|
g_free (start_time_str);
|
|
|
|
if (*url == NULL)
|
|
return GST_FLOW_ERROR;
|
|
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
GstClockTime
|
|
gst_mss_stream_get_fragment_gst_timestamp (GstMssStream * stream)
|
|
{
|
|
guint64 time;
|
|
guint64 timescale;
|
|
GstMssStreamFragment *fragment;
|
|
|
|
g_return_val_if_fail (stream->active, GST_CLOCK_TIME_NONE);
|
|
|
|
if (!stream->current_fragment) {
|
|
GList *last = g_list_last (stream->fragments);
|
|
if (last == NULL)
|
|
return GST_CLOCK_TIME_NONE;
|
|
|
|
fragment = last->data;
|
|
time = fragment->time + (fragment->duration * fragment->repetitions);
|
|
} else {
|
|
fragment = stream->current_fragment->data;
|
|
time =
|
|
fragment->time +
|
|
(fragment->duration * stream->fragment_repetition_index);
|
|
}
|
|
|
|
timescale = gst_mss_stream_get_timescale (stream);
|
|
return (GstClockTime) gst_util_uint64_scale_round (time, GST_SECOND,
|
|
timescale);
|
|
}
|
|
|
|
GstClockTime
|
|
gst_mss_stream_get_fragment_gst_duration (GstMssStream * stream)
|
|
{
|
|
guint64 dur;
|
|
guint64 timescale;
|
|
GstMssStreamFragment *fragment;
|
|
|
|
g_return_val_if_fail (stream->active, GST_FLOW_ERROR);
|
|
|
|
if (!stream->current_fragment)
|
|
return GST_CLOCK_TIME_NONE;
|
|
|
|
fragment = stream->current_fragment->data;
|
|
|
|
dur = fragment->duration;
|
|
timescale = gst_mss_stream_get_timescale (stream);
|
|
return (GstClockTime) gst_util_uint64_scale_round (dur, GST_SECOND,
|
|
timescale);
|
|
}
|
|
|
|
gboolean
|
|
gst_mss_stream_has_next_fragment (GstMssStream * stream)
|
|
{
|
|
g_return_val_if_fail (stream->active, FALSE);
|
|
|
|
if (stream->current_fragment == NULL)
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
GstFlowReturn
|
|
gst_mss_stream_advance_fragment (GstMssStream * stream)
|
|
{
|
|
GstMssStreamFragment *fragment;
|
|
const gchar *stream_type_name =
|
|
gst_mss_stream_type_name (gst_mss_stream_get_type (stream));
|
|
|
|
g_return_val_if_fail (stream->active, GST_FLOW_ERROR);
|
|
|
|
if (stream->current_fragment == NULL)
|
|
return GST_FLOW_EOS;
|
|
|
|
fragment = stream->current_fragment->data;
|
|
stream->fragment_repetition_index++;
|
|
if (stream->fragment_repetition_index < fragment->repetitions)
|
|
goto beach;
|
|
|
|
stream->fragment_repetition_index = 0;
|
|
stream->current_fragment = g_list_next (stream->current_fragment);
|
|
|
|
GST_DEBUG ("Advanced to fragment #%d on %s stream", fragment->number,
|
|
stream_type_name);
|
|
if (stream->current_fragment == NULL)
|
|
return GST_FLOW_EOS;
|
|
|
|
beach:
|
|
gst_mss_fragment_parser_clear (&stream->fragment_parser);
|
|
gst_mss_fragment_parser_init (&stream->fragment_parser);
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
GstFlowReturn
|
|
gst_mss_stream_regress_fragment (GstMssStream * stream)
|
|
{
|
|
GstMssStreamFragment *fragment;
|
|
g_return_val_if_fail (stream->active, GST_FLOW_ERROR);
|
|
|
|
if (stream->current_fragment == NULL)
|
|
return GST_FLOW_EOS;
|
|
|
|
fragment = stream->current_fragment->data;
|
|
if (stream->fragment_repetition_index == 0) {
|
|
stream->current_fragment = g_list_previous (stream->current_fragment);
|
|
if (stream->current_fragment == NULL)
|
|
return GST_FLOW_EOS;
|
|
fragment = stream->current_fragment->data;
|
|
stream->fragment_repetition_index = fragment->repetitions - 1;
|
|
} else {
|
|
stream->fragment_repetition_index--;
|
|
}
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
const gchar *
|
|
gst_mss_stream_type_name (GstMssStreamType streamtype)
|
|
{
|
|
switch (streamtype) {
|
|
case MSS_STREAM_TYPE_VIDEO:
|
|
return "video";
|
|
case MSS_STREAM_TYPE_AUDIO:
|
|
return "audio";
|
|
case MSS_STREAM_TYPE_UNKNOWN:
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Seeks all streams to the fragment that contains the set time
|
|
*
|
|
* @forward: if this is forward playback
|
|
* @time: time in nanoseconds
|
|
*/
|
|
void
|
|
gst_mss_manifest_seek (GstMssManifest * manifest, gboolean forward,
|
|
guint64 time)
|
|
{
|
|
GSList *iter;
|
|
|
|
for (iter = manifest->streams; iter; iter = g_slist_next (iter)) {
|
|
gst_mss_stream_seek (iter->data, forward, 0, time, NULL);
|
|
}
|
|
}
|
|
|
|
#define SNAP_AFTER(forward,flags) \
|
|
((forward && (flags & GST_SEEK_FLAG_SNAP_AFTER)) || \
|
|
(!forward && (flags & GST_SEEK_FLAG_SNAP_BEFORE)))
|
|
|
|
/**
|
|
* Seeks this stream to the fragment that contains the sample at time
|
|
*
|
|
* @time: time in nanoseconds
|
|
*/
|
|
void
|
|
gst_mss_stream_seek (GstMssStream * stream, gboolean forward,
|
|
GstSeekFlags flags, guint64 time, guint64 * final_time)
|
|
{
|
|
GList *iter;
|
|
guint64 timescale;
|
|
GstMssStreamFragment *fragment = NULL;
|
|
|
|
timescale = gst_mss_stream_get_timescale (stream);
|
|
time = gst_util_uint64_scale_round (time, timescale, GST_SECOND);
|
|
|
|
GST_DEBUG ("Stream %s seeking to %" G_GUINT64_FORMAT, stream->url, time);
|
|
for (iter = stream->fragments; iter; iter = g_list_next (iter)) {
|
|
fragment = iter->data;
|
|
if (stream->has_live_fragments) {
|
|
if (fragment->time + fragment->repetitions * fragment->duration > time)
|
|
stream->current_fragment = iter;
|
|
break;
|
|
}
|
|
if (fragment->time + fragment->repetitions * fragment->duration > time) {
|
|
stream->current_fragment = iter;
|
|
stream->fragment_repetition_index =
|
|
(time - fragment->time) / fragment->duration;
|
|
if (((time - fragment->time) % fragment->duration) == 0) {
|
|
|
|
/* for reverse playback, start from the previous fragment when we are
|
|
* exactly at a limit */
|
|
if (!forward)
|
|
stream->fragment_repetition_index--;
|
|
} else if (SNAP_AFTER (forward, flags))
|
|
stream->fragment_repetition_index++;
|
|
|
|
if (stream->fragment_repetition_index == fragment->repetitions) {
|
|
/* move to the next one */
|
|
stream->fragment_repetition_index = 0;
|
|
stream->current_fragment = g_list_next (iter);
|
|
fragment =
|
|
stream->current_fragment ? stream->current_fragment->data : NULL;
|
|
|
|
} else if (stream->fragment_repetition_index == -1) {
|
|
if (g_list_previous (iter)) {
|
|
stream->current_fragment = g_list_previous (iter);
|
|
fragment = stream->current_fragment->data;
|
|
g_assert (fragment);
|
|
stream->fragment_repetition_index = fragment->repetitions - 1;
|
|
} else {
|
|
stream->fragment_repetition_index = 0;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
GST_DEBUG ("Stream %s seeked to fragment time %" G_GUINT64_FORMAT
|
|
" repetition %u", stream->url,
|
|
fragment ? fragment->time : GST_CLOCK_TIME_NONE,
|
|
stream->fragment_repetition_index);
|
|
if (final_time) {
|
|
if (fragment) {
|
|
*final_time = gst_util_uint64_scale_round (fragment->time +
|
|
stream->fragment_repetition_index * fragment->duration,
|
|
GST_SECOND, timescale);
|
|
} else {
|
|
GstMssStreamFragment *last_fragment = g_list_last (iter)->data;
|
|
*final_time = gst_util_uint64_scale_round (last_fragment->time +
|
|
last_fragment->repetitions * last_fragment->duration,
|
|
GST_SECOND, timescale);
|
|
}
|
|
}
|
|
}
|
|
|
|
guint64
|
|
gst_mss_manifest_get_current_bitrate (GstMssManifest * manifest)
|
|
{
|
|
guint64 bitrate = 0;
|
|
GSList *iter;
|
|
|
|
for (iter = gst_mss_manifest_get_streams (manifest); iter;
|
|
iter = g_slist_next (iter)) {
|
|
GstMssStream *stream = iter->data;
|
|
if (stream->active && stream->current_quality) {
|
|
GstMssStreamQuality *q = stream->current_quality->data;
|
|
|
|
bitrate += q->bitrate;
|
|
}
|
|
}
|
|
|
|
return bitrate;
|
|
}
|
|
|
|
gboolean
|
|
gst_mss_manifest_is_live (GstMssManifest * manifest)
|
|
{
|
|
return manifest->is_live;
|
|
}
|
|
|
|
static void
|
|
gst_mss_stream_reload_fragments (GstMssStream * stream, xmlNodePtr streamIndex)
|
|
{
|
|
xmlNodePtr iter;
|
|
guint64 current_gst_time;
|
|
GstMssFragmentListBuilder builder;
|
|
|
|
if (stream->has_live_fragments)
|
|
return;
|
|
|
|
current_gst_time = gst_mss_stream_get_fragment_gst_timestamp (stream);
|
|
|
|
gst_mss_fragment_list_builder_init (&builder);
|
|
|
|
GST_DEBUG ("Current position: %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (current_gst_time));
|
|
|
|
for (iter = streamIndex->children; iter; iter = iter->next) {
|
|
if (node_has_type (iter, MSS_NODE_STREAM_FRAGMENT)) {
|
|
gst_mss_fragment_list_builder_add (&builder, iter);
|
|
} else {
|
|
/* TODO gst log this */
|
|
}
|
|
}
|
|
|
|
/* store the new fragments list */
|
|
if (builder.fragments) {
|
|
g_list_free_full (stream->fragments, g_free);
|
|
stream->fragments = g_list_reverse (builder.fragments);
|
|
stream->current_fragment = stream->fragments;
|
|
/* TODO Verify how repositioning here works for reverse
|
|
* playback - it might start from the wrong fragment */
|
|
gst_mss_stream_seek (stream, TRUE, 0, current_gst_time, NULL);
|
|
}
|
|
}
|
|
|
|
static void
|
|
gst_mss_manifest_reload_fragments_from_xml (GstMssManifest * manifest,
|
|
xmlNodePtr root)
|
|
{
|
|
xmlNodePtr nodeiter;
|
|
GSList *streams = manifest->streams;
|
|
|
|
/* we assume the server is providing the streams in the same order in
|
|
* every manifest */
|
|
for (nodeiter = root->children; nodeiter && streams;
|
|
nodeiter = nodeiter->next) {
|
|
if (nodeiter->type == XML_ELEMENT_NODE
|
|
&& (strcmp ((const char *) nodeiter->name, "StreamIndex") == 0)) {
|
|
gst_mss_stream_reload_fragments (streams->data, nodeiter);
|
|
streams = g_slist_next (streams);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
gst_mss_manifest_reload_fragments (GstMssManifest * manifest, GstBuffer * data)
|
|
{
|
|
xmlDocPtr xml;
|
|
xmlNodePtr root;
|
|
GstMapInfo info;
|
|
|
|
gst_buffer_map (data, &info, GST_MAP_READ);
|
|
|
|
xml = xmlReadMemory ((const gchar *) info.data,
|
|
info.size, "manifest", NULL, 0);
|
|
root = xmlDocGetRootElement (xml);
|
|
|
|
gst_mss_manifest_reload_fragments_from_xml (manifest, root);
|
|
|
|
xmlFreeDoc (xml);
|
|
|
|
gst_buffer_unmap (data, &info);
|
|
}
|
|
|
|
gboolean
|
|
gst_mss_stream_select_bitrate (GstMssStream * stream, guint64 bitrate)
|
|
{
|
|
GList *iter = stream->current_quality;
|
|
GList *next;
|
|
GstMssStreamQuality *q = iter->data;
|
|
|
|
while (q->bitrate > bitrate) {
|
|
next = g_list_previous (iter);
|
|
if (next) {
|
|
iter = next;
|
|
q = iter->data;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
while (q->bitrate < bitrate) {
|
|
GstMssStreamQuality *next_q;
|
|
next = g_list_next (iter);
|
|
if (next) {
|
|
next_q = next->data;
|
|
if (next_q->bitrate < bitrate) {
|
|
iter = next;
|
|
q = iter->data;
|
|
} else {
|
|
break;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (iter == stream->current_quality)
|
|
return FALSE;
|
|
stream->current_quality = iter;
|
|
return TRUE;
|
|
}
|
|
|
|
guint64
|
|
gst_mss_stream_get_current_bitrate (GstMssStream * stream)
|
|
{
|
|
GstMssStreamQuality *q;
|
|
if (stream->current_quality == NULL)
|
|
return 0;
|
|
|
|
q = stream->current_quality->data;
|
|
return q->bitrate;
|
|
}
|
|
|
|
/**
|
|
* gst_mss_manifest_change_bitrate:
|
|
* @manifest: the manifest
|
|
* @bitrate: the maximum bitrate to use (bps)
|
|
*
|
|
* Iterates over the active streams and changes their bitrates to the maximum
|
|
* value so that the bitrates of all streams are not larger than
|
|
* @bitrate.
|
|
*
|
|
* Return: %TRUE if any stream changed its bitrate
|
|
*/
|
|
gboolean
|
|
gst_mss_manifest_change_bitrate (GstMssManifest * manifest, guint64 bitrate)
|
|
{
|
|
gboolean ret = FALSE;
|
|
GSList *iter;
|
|
|
|
/* TODO This algorithm currently sets the same bitrate for all streams,
|
|
* it should actually use the sum of all streams bitrates to compare to
|
|
* the target value */
|
|
|
|
if (bitrate == 0) {
|
|
/* use maximum */
|
|
bitrate = G_MAXUINT64;
|
|
}
|
|
|
|
for (iter = gst_mss_manifest_get_streams (manifest); iter;
|
|
iter = g_slist_next (iter)) {
|
|
GstMssStream *stream = iter->data;
|
|
if (stream->active) {
|
|
ret = ret | gst_mss_stream_select_bitrate (stream, bitrate);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static GstBuffer *
|
|
gst_buffer_from_hex_string (const gchar * s)
|
|
{
|
|
GstBuffer *buffer = NULL;
|
|
gint len;
|
|
gchar ts[3];
|
|
guint8 *data;
|
|
gint i;
|
|
GstMapInfo info;
|
|
|
|
len = strlen (s);
|
|
if (len & 1)
|
|
return NULL;
|
|
|
|
buffer = gst_buffer_new_and_alloc (len / 2);
|
|
gst_buffer_map (buffer, &info, GST_MAP_WRITE);
|
|
data = info.data;
|
|
for (i = 0; i < len / 2; i++) {
|
|
if (!isxdigit ((int) s[i * 2]) || !isxdigit ((int) s[i * 2 + 1])) {
|
|
gst_buffer_unref (buffer);
|
|
return NULL;
|
|
}
|
|
|
|
ts[0] = s[i * 2 + 0];
|
|
ts[1] = s[i * 2 + 1];
|
|
ts[2] = 0;
|
|
|
|
data[i] = (guint8) strtoul (ts, NULL, 16);
|
|
}
|
|
|
|
gst_buffer_unmap (buffer, &info);
|
|
return buffer;
|
|
}
|
|
|
|
const gchar *
|
|
gst_mss_stream_get_lang (GstMssStream * stream)
|
|
{
|
|
return stream->lang;
|
|
}
|
|
|
|
static GstClockTime
|
|
gst_mss_manifest_get_dvr_window_length_clock_time (GstMssManifest * manifest)
|
|
{
|
|
gint64 timescale;
|
|
|
|
/* the entire file is always available for non-live streams */
|
|
if (manifest->dvr_window == 0)
|
|
return GST_CLOCK_TIME_NONE;
|
|
|
|
timescale = gst_mss_manifest_get_timescale (manifest);
|
|
return (GstClockTime) gst_util_uint64_scale_round (manifest->dvr_window,
|
|
GST_SECOND, timescale);
|
|
}
|
|
|
|
static gboolean
|
|
gst_mss_stream_get_live_seek_range (GstMssStream * stream, gint64 * start,
|
|
gint64 * stop)
|
|
{
|
|
GList *l;
|
|
GstMssStreamFragment *fragment;
|
|
guint64 timescale = gst_mss_stream_get_timescale (stream);
|
|
|
|
g_return_val_if_fail (stream->active, FALSE);
|
|
|
|
/* XXX: assumes all the data in the stream is still available */
|
|
l = g_list_first (stream->fragments);
|
|
fragment = (GstMssStreamFragment *) l->data;
|
|
*start = gst_util_uint64_scale_round (fragment->time, GST_SECOND, timescale);
|
|
|
|
l = g_list_last (stream->fragments);
|
|
fragment = (GstMssStreamFragment *) l->data;
|
|
*stop = gst_util_uint64_scale_round (fragment->time + fragment->duration *
|
|
fragment->repetitions, GST_SECOND, timescale);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
gst_mss_manifest_get_live_seek_range (GstMssManifest * manifest, gint64 * start,
|
|
gint64 * stop)
|
|
{
|
|
GSList *iter;
|
|
gboolean ret = FALSE;
|
|
|
|
for (iter = manifest->streams; iter; iter = g_slist_next (iter)) {
|
|
GstMssStream *stream = iter->data;
|
|
|
|
if (stream->active) {
|
|
/* FIXME: bound this correctly for multiple streams */
|
|
if (!(ret = gst_mss_stream_get_live_seek_range (stream, start, stop)))
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ret && gst_mss_manifest_is_live (manifest)) {
|
|
GstClockTime dvr_window =
|
|
gst_mss_manifest_get_dvr_window_length_clock_time (manifest);
|
|
|
|
if (GST_CLOCK_TIME_IS_VALID (dvr_window) && *stop - *start > dvr_window) {
|
|
*start = *stop - dvr_window;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void
|
|
gst_mss_manifest_live_adapter_push (GstMssStream * stream, GstBuffer * buffer)
|
|
{
|
|
gst_adapter_push (stream->live_adapter, buffer);
|
|
}
|
|
|
|
gsize
|
|
gst_mss_manifest_live_adapter_available (GstMssStream * stream)
|
|
{
|
|
return gst_adapter_available (stream->live_adapter);
|
|
}
|
|
|
|
GstBuffer *
|
|
gst_mss_manifest_live_adapter_take_buffer (GstMssStream * stream, gsize nbytes)
|
|
{
|
|
return gst_adapter_take_buffer (stream->live_adapter, nbytes);
|
|
}
|
|
|
|
gboolean
|
|
gst_mss_stream_fragment_parsing_needed (GstMssStream * stream)
|
|
{
|
|
return stream->fragment_parser.status == GST_MSS_FRAGMENT_HEADER_PARSER_INIT;
|
|
}
|
|
|
|
void
|
|
gst_mss_stream_parse_fragment (GstMssStream * stream, GstBuffer * buffer)
|
|
{
|
|
GstMssStreamFragment *current_fragment = NULL;
|
|
const gchar *stream_type_name;
|
|
guint8 index;
|
|
|
|
if (!stream->has_live_fragments)
|
|
return;
|
|
|
|
if (!gst_mss_fragment_parser_add_buffer (&stream->fragment_parser, buffer))
|
|
return;
|
|
|
|
current_fragment = stream->current_fragment->data;
|
|
current_fragment->time = stream->fragment_parser.tfxd.time;
|
|
current_fragment->duration = stream->fragment_parser.tfxd.duration;
|
|
|
|
stream_type_name =
|
|
gst_mss_stream_type_name (gst_mss_stream_get_type (stream));
|
|
|
|
for (index = 0; index < stream->fragment_parser.tfrf.entries_count; index++) {
|
|
GList *l = g_list_last (stream->fragments);
|
|
GstMssStreamFragment *last;
|
|
GstMssStreamFragment *fragment;
|
|
|
|
if (l == NULL)
|
|
break;
|
|
|
|
last = (GstMssStreamFragment *) l->data;
|
|
|
|
if (last->time == stream->fragment_parser.tfrf.entries[index].time)
|
|
continue;
|
|
|
|
fragment = g_new (GstMssStreamFragment, 1);
|
|
fragment->number = last->number + 1;
|
|
fragment->repetitions = 1;
|
|
fragment->time = stream->fragment_parser.tfrf.entries[index].time;
|
|
fragment->duration = stream->fragment_parser.tfrf.entries[index].duration;
|
|
|
|
stream->fragments = g_list_append (stream->fragments, fragment);
|
|
GST_LOG ("Adding fragment number: %u to %s stream, time: %" G_GUINT64_FORMAT
|
|
", duration: %" G_GUINT64_FORMAT ", repetitions: %u",
|
|
fragment->number, stream_type_name,
|
|
fragment->time, fragment->duration, fragment->repetitions);
|
|
}
|
|
}
|