You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

640 lines
22 KiB

/*
* Parser.cpp
*****************************************************************************
* Copyright © 2015 - VideoLAN and VLC Authors
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "Parser.hpp"
#include "HLSSegment.hpp"
#include "Representation.hpp"
#include "../../adaptive/SharedResources.hpp"
#include "../../adaptive/playlist/BasePeriod.h"
#include "../../adaptive/playlist/BaseAdaptationSet.h"
#include "../../adaptive/playlist/SegmentList.h"
#include "../../adaptive/encryption/Keyring.hpp"
#include "../../adaptive/tools/Retrieve.hpp"
#include "../../adaptive/tools/Helper.h"
#include "../../adaptive/tools/Conversions.hpp"
#include "../../adaptive/logic/BufferingLogic.hpp"
#include "M3U8.hpp"
#include "Tags.hpp"
#include <vlc_strings.h>
#include <vlc_stream.h>
#include <cstdio>
#include <sstream>
#include <map>
#include <cctype>
#include <algorithm>
using namespace adaptive;
using namespace adaptive::playlist;
using namespace hls::playlist;
M3U8Parser::M3U8Parser(SharedResources *res)
{
resources = res;
}
M3U8Parser::~M3U8Parser ()
{
}
static std::list<Tag *> getTagsFromList(std::list<Tag *> &list, int tag)
{
std::list<Tag *> ret;
std::list<Tag *>::const_iterator it;
for(it = list.begin(); it != list.end(); ++it)
{
if( (*it)->getType() == tag )
ret.push_back(*it);
}
return ret;
}
static Tag * getTagFromList(std::list<Tag *> &list, int tag)
{
std::list<Tag *>::const_iterator it;
for(it = list.begin(); it != list.end(); ++it)
{
if( (*it)->getType() == tag )
return *it;
}
return NULL;
}
static void releaseTagsList(std::list<Tag *> &list)
{
std::list<Tag *>::const_iterator it;
for(it = list.begin(); it != list.end(); ++it)
delete *it;
list.clear();
}
Representation * M3U8Parser::createRepresentation(BaseAdaptationSet *adaptSet, const AttributesTag * tag)
{
const Attribute *uriAttr = tag->getAttributeByName("URI");
const Attribute *bwAttr = tag->getAttributeByName("BANDWIDTH");
const Attribute *resAttr = tag->getAttributeByName("RESOLUTION");
Representation *rep = new (std::nothrow) Representation(adaptSet);
if(rep)
{
if(uriAttr)
{
std::string uri;
if(tag->getType() == AttributesTag::EXTXMEDIA)
{
uri = uriAttr->quotedString();
}
else
{
uri = uriAttr->value;
}
rep->setID(uri);
rep->setPlaylistUrl(uri);
if(uri.find('/') != std::string::npos)
{
uri = Helper::getDirectoryPath(uri);
if(!uri.empty())
rep->baseUrl.Set(new Url(uri.append("/")));
}
}
if(bwAttr)
rep->setBandwidth(bwAttr->decimal());
if(tag->getAttributeByName("CODECS"))
rep->addCodecs(tag->getAttributeByName("CODECS")->quotedString());
if(resAttr)
{
std::pair<int, int> res = resAttr->getResolution();
if(res.first && res.second)
{
rep->setWidth(res.first);
rep->setHeight(res.second);
}
}
}
return rep;
}
void M3U8Parser::createAndFillRepresentation(vlc_object_t *p_obj, BaseAdaptationSet *adaptSet,
const AttributesTag *tag,
const std::list<Tag *> &tagslist)
{
Representation *rep = createRepresentation(adaptSet, tag);
if(rep)
{
parseSegments(p_obj, rep, tagslist);
if(rep->isLive())
{
/* avoid update playlist immediately */
logic::DefaultBufferingLogic buflogic;
uint64_t startseq = buflogic.getStartSegmentNumber(rep);
rep->scheduleNextUpdate(startseq);
}
adaptSet->addRepresentation(rep);
}
}
bool M3U8Parser::appendSegmentsFromPlaylistURI(vlc_object_t *p_obj, Representation *rep)
{
block_t *p_block = Retrieve::HTTP(resources, rep->getPlaylistUrl().toString());
if(p_block)
{
stream_t *substream = vlc_stream_MemoryNew(p_obj, p_block->p_buffer, p_block->i_buffer, true);
if(substream)
{
std::list<Tag *> tagslist = parseEntries(substream);
vlc_stream_Delete(substream);
parseSegments(p_obj, rep, tagslist);
releaseTagsList(tagslist);
}
block_Release(p_block);
return true;
}
return false;
}
static bool parseEncryption(const AttributesTag *keytag, const Url &playlistUrl,
CommonEncryption &encryption)
{
if( keytag->getAttributeByName("METHOD") &&
keytag->getAttributeByName("METHOD")->value == "AES-128" &&
keytag->getAttributeByName("URI") )
{
encryption.method = CommonEncryption::Method::AES_128;
encryption.uri.clear();
Url keyurl(keytag->getAttributeByName("URI")->quotedString());
if(!keyurl.hasScheme())
{
keyurl.prepend(Helper::getDirectoryPath(playlistUrl.toString()).append("/"));
}
encryption.uri = keyurl.toString();
if(keytag->getAttributeByName("IV"))
{
encryption.iv.clear();
encryption.iv = keytag->getAttributeByName("IV")->hexSequence();
}
return true;
}
else
{
/* unsupported or invalid */
encryption.method = CommonEncryption::Method::NONE;
encryption.uri.clear();
encryption.iv.clear();
return false;
}
}
void M3U8Parser::parseSegments(vlc_object_t *, Representation *rep, const std::list<Tag *> &tagslist)
{
SegmentList *segmentList = new (std::nothrow) SegmentList(rep);
rep->setTimescale(100);
rep->b_loaded = true;
vlc_tick_t totalduration = 0;
vlc_tick_t nzStartTime = 0;
vlc_tick_t absReferenceTime = VLC_TICK_INVALID;
uint64_t sequenceNumber = 0;
bool discontinuity = false;
std::size_t prevbyterangeoffset = 0;
const SingleValueTag *ctx_byterange = NULL;
CommonEncryption encryption;
const ValuesListTag *ctx_extinf = NULL;
std::list<Tag *>::const_iterator it;
for(it = tagslist.begin(); it != tagslist.end(); ++it)
{
const Tag *tag = *it;
switch(tag->getType())
{
/* using static cast as attribute type permits avoiding class check */
case SingleValueTag::EXTXMEDIASEQUENCE:
{
sequenceNumber = (static_cast<const SingleValueTag*>(tag))->getValue().decimal();
}
break;
case ValuesListTag::EXTINF:
{
ctx_extinf = static_cast<const ValuesListTag *>(tag);
}
break;
case SingleValueTag::URI:
{
const SingleValueTag *uritag = static_cast<const SingleValueTag *>(tag);
if(uritag->getValue().value.empty())
{
ctx_extinf = NULL;
ctx_byterange = NULL;
break;
}
HLSSegment *segment = new (std::nothrow) HLSSegment(rep, sequenceNumber++);
if(!segment)
break;
segment->setSourceUrl(uritag->getValue().value);
/* Need to use EXTXTARGETDURATION as default as some can't properly set segment one */
double duration = rep->targetDuration;
if(ctx_extinf)
{
const Attribute *durAttribute = ctx_extinf->getAttributeByName("DURATION");
if(durAttribute)
duration = durAttribute->floatingPoint();
ctx_extinf = NULL;
}
const vlc_tick_t nzDuration = vlc_tick_from_sec( duration );
segment->duration.Set(duration * (uint64_t) rep->getTimescale());
segment->startTime.Set(rep->getTimescale().ToScaled(nzStartTime));
nzStartTime += nzDuration;
totalduration += nzDuration;
if(absReferenceTime != VLC_TICK_INVALID)
{
segment->utcTime = absReferenceTime;
absReferenceTime += nzDuration;
}
segmentList->addSegment(segment);
if(ctx_byterange)
{
std::pair<std::size_t,std::size_t> range = ctx_byterange->getValue().getByteRange();
if(range.first == 0) /* first == size, second = offset */
range.first = prevbyterangeoffset;
prevbyterangeoffset = range.first + range.second;
segment->setByteRange(range.first, prevbyterangeoffset - 1);
ctx_byterange = NULL;
}
if(discontinuity)
{
segment->discontinuity = true;
discontinuity = false;
}
if(encryption.method != CommonEncryption::Method::NONE)
segment->setEncryption(encryption);
}
break;
case SingleValueTag::EXTXTARGETDURATION:
rep->targetDuration = static_cast<const SingleValueTag *>(tag)->getValue().decimal();
break;
case SingleValueTag::EXTXPLAYLISTTYPE:
rep->b_live = (static_cast<const SingleValueTag *>(tag)->getValue().value != "VOD");
break;
case SingleValueTag::EXTXBYTERANGE:
ctx_byterange = static_cast<const SingleValueTag *>(tag);
break;
case SingleValueTag::EXTXPROGRAMDATETIME:
rep->b_consistent = false;
absReferenceTime = VLC_TICK_0 +
UTCTime(static_cast<const SingleValueTag *>(tag)->getValue().value).mtime();
break;
case AttributesTag::EXTXKEY:
parseEncryption(static_cast<const AttributesTag *>(tag),
rep->getPlaylistUrl(), encryption);
break;
case AttributesTag::EXTXMAP:
{
const AttributesTag *keytag = static_cast<const AttributesTag *>(tag);
const Attribute *uriAttr;
if(keytag && (uriAttr = keytag->getAttributeByName("URI")) &&
!segmentList->initialisationSegment.Get()) /* FIXME: handle discontinuities */
{
InitSegment *initSegment = new (std::nothrow) InitSegment(rep);
if(initSegment)
{
initSegment->setSourceUrl(uriAttr->quotedString());
const Attribute *byterangeAttr = keytag->getAttributeByName("BYTERANGE");
if(byterangeAttr)
{
const std::pair<std::size_t,std::size_t> range = byterangeAttr->unescapeQuotes().getByteRange();
initSegment->setByteRange(range.first, range.first + range.second - 1);
}
segmentList->initialisationSegment.Set(initSegment);
}
}
}
break;
case Tag::EXTXDISCONTINUITY:
discontinuity = true;
break;
case Tag::EXTXENDLIST:
rep->b_live = false;
break;
}
}
if(rep->isLive())
{
rep->getPlaylist()->duration.Set(0);
}
else if(totalduration > rep->getPlaylist()->duration.Get())
{
rep->getPlaylist()->duration.Set(totalduration);
}
rep->updateSegmentList(segmentList, true);
}
M3U8 * M3U8Parser::parse(vlc_object_t *p_object, stream_t *p_stream, const std::string &playlisturl)
{
char *psz_line = vlc_stream_ReadLine(p_stream);
if(!psz_line || strncmp(psz_line, "#EXTM3U", 7) ||
(psz_line[7] && !std::isspace(psz_line[7])))
{
free(psz_line);
return NULL;
}
free(psz_line);
M3U8 *playlist = new (std::nothrow) M3U8(p_object);
if(!playlist)
return NULL;
if(!playlisturl.empty())
playlist->setPlaylistUrl( Helper::getDirectoryPath(playlisturl).append("/") );
BasePeriod *period = new (std::nothrow) BasePeriod( playlist );
if(!period)
return playlist;
std::list<Tag *> tagslist = parseEntries(p_stream);
bool b_masterplaylist = !getTagsFromList(tagslist, AttributesTag::EXTXSTREAMINF).empty();
if(b_masterplaylist)
{
std::list<Tag *>::const_iterator it;
std::map<std::string, AttributesTag *> groupsmap;
/* Preload Session Key */
Tag *sessionKey = getTagFromList(tagslist, AttributesTag::EXTXSESSIONKEY);
if(sessionKey)
{
CommonEncryption sessionEncryption;
if(parseEncryption(static_cast<const AttributesTag *>(sessionKey),
playlist->getUrlSegment(), sessionEncryption) &&
!sessionEncryption.uri.empty())
{
resources->getKeyring()->getKey(resources, sessionEncryption.uri);
}
}
/* We'll need to create an adaptation set for each media group / alternative rendering
* we create a list of playlist being and alternative/group */
std::list<Tag *> mediainfotags = getTagsFromList(tagslist, AttributesTag::EXTXMEDIA);
for(it = mediainfotags.begin(); it != mediainfotags.end(); ++it)
{
AttributesTag *tag = dynamic_cast<AttributesTag *>(*it);
if(tag && tag->getAttributeByName("URI"))
{
std::pair<std::string, AttributesTag *> pair(tag->getAttributeByName("URI")->quotedString(), tag);
groupsmap.insert(pair);
}
}
/* Then we parse all playlists uri and add them, except when alternative */
BaseAdaptationSet *adaptSet = new (std::nothrow) BaseAdaptationSet(period);
if(adaptSet)
{
/* adaptSet->setSegmentAligned(true); FIXME: based on streamformat */
std::list<Tag *> streaminfotags = getTagsFromList(tagslist, AttributesTag::EXTXSTREAMINF);
for(it = streaminfotags.begin(); it != streaminfotags.end(); ++it)
{
AttributesTag *tag = dynamic_cast<AttributesTag *>(*it);
if(tag && tag->getAttributeByName("URI"))
{
if(groupsmap.find(tag->getAttributeByName("URI")->value) == groupsmap.end())
{
/* not a group, belong to default adaptation set */
Representation *rep = createRepresentation(adaptSet, tag);
if(rep)
{
adaptSet->addRepresentation(rep);
}
}
}
if(adaptSet->description.Get().empty() &&
tag && tag->getAttributeByName("NAME"))
{
adaptSet->description.Set(tag->getAttributeByName("NAME")->quotedString());
}
}
if(!adaptSet->getRepresentations().empty())
period->addAdaptationSet(adaptSet);
else
delete adaptSet;
}
/* Finally add all groups */
unsigned set_id = 1;
std::map<std::string, AttributesTag *>::const_iterator groupsit;
for(groupsit = groupsmap.begin(); groupsit != groupsmap.end(); ++groupsit)
{
std::pair<std::string, AttributesTag *> pair = *groupsit;
if(!pair.second->getAttributeByName("TYPE"))
continue;
BaseAdaptationSet *altAdaptSet = new (std::nothrow) BaseAdaptationSet(period);
if(altAdaptSet)
{
Representation *rep = createRepresentation(altAdaptSet, pair.second);
if(rep)
{
altAdaptSet->addRepresentation(rep);
}
std::string desc;
if(pair.second->getAttributeByName("GROUP-ID"))
desc = pair.second->getAttributeByName("GROUP-ID")->quotedString();
if(pair.second->getAttributeByName("NAME"))
{
if(!desc.empty())
desc += " ";
desc += pair.second->getAttributeByName("NAME")->quotedString();
}
if(pair.second->getAttributeByName("CODECS"))
{
rep->addCodecs(pair.second->getAttributeByName("CODECS")->quotedString());
}
else
{
if(pair.second->getAttributeByName("TYPE")->value == "AUDIO")
rep->addCodecs("mp4a");
else if(pair.second->getAttributeByName("TYPE")->value == "VIDEO")
rep->addCodecs("avc1");
else if(pair.second->getAttributeByName("TYPE")->value == "SUBTITLES")
rep->addCodecs("wvtt");
}
if(!desc.empty())
{
altAdaptSet->description.Set(desc);
altAdaptSet->setID(ID(desc));
}
else altAdaptSet->setID(ID(set_id++));
if(pair.second->getAttributeByName("DEFAULT"))
{
if(pair.second->getAttributeByName("DEFAULT")->value == "YES")
altAdaptSet->setRole(Role(Role::ROLE_MAIN));
else
altAdaptSet->setRole(Role(Role::ROLE_ALTERNATE));
}
if(pair.second->getAttributeByName("AUTOSELECT"))
{
if(pair.second->getAttributeByName("AUTOSELECT")->value == "NO" &&
!pair.second->getAttributeByName("DEFAULT"))
altAdaptSet->setRole(Role(Role::ROLE_SUPPLEMENTARY));
}
/* Subtitles unsupported for now */
const Attribute *typeattr = pair.second->getAttributeByName("TYPE");
if(typeattr->value == "SUBTITLES")
{
altAdaptSet->setRole(Role(Role::ROLE_SUBTITLE));
rep->streamFormat = StreamFormat(StreamFormat::UNSUPPORTED);
}
else if(typeattr->value != "AUDIO" && typeattr->value != "VIDEO")
{
rep->streamFormat = StreamFormat(StreamFormat::UNSUPPORTED);
}
if(pair.second->getAttributeByName("LANGUAGE"))
altAdaptSet->setLang(pair.second->getAttributeByName("LANGUAGE")->quotedString());
if(!altAdaptSet->getRepresentations().empty())
period->addAdaptationSet(altAdaptSet);
else
delete altAdaptSet;
}
}
}
else /* Non master playlist (opened directly subplaylist or HLS v1) */
{
BaseAdaptationSet *adaptSet = new (std::nothrow) BaseAdaptationSet(period);
if(adaptSet)
{
AttributesTag *tag = new AttributesTag(AttributesTag::EXTXSTREAMINF, "");
tag->addAttribute(new Attribute("URI", playlisturl));
createAndFillRepresentation(p_object, adaptSet, tag, tagslist);
if(!adaptSet->getRepresentations().empty())
period->addAdaptationSet(adaptSet);
else
delete adaptSet;
delete tag;
}
}
playlist->addPeriod(period);
releaseTagsList(tagslist);
playlist->debug();
return playlist;
}
std::list<Tag *> M3U8Parser::parseEntries(stream_t *stream)
{
std::list<Tag *> entrieslist;
Tag *lastTag = NULL;
char *psz_line;
while((psz_line = vlc_stream_ReadLine(stream)))
{
if(*psz_line == '#')
{
if(!strncmp(psz_line, "#EXT", 4)) //tag
{
std::string key;
std::string attributes;
const char *split = strchr(psz_line, ':');
if(split)
{
key = std::string(psz_line + 1, split - psz_line - 1);
attributes = std::string(split + 1);
}
else
{
key = std::string(psz_line + 1);
}
if(!key.empty())
{
Tag *tag = TagFactory::createTagByName(key, attributes);
if(tag)
entrieslist.push_back(tag);
lastTag = tag;
}
}
}
else if(*psz_line)
{
/* URI */
if(lastTag && lastTag->getType() == AttributesTag::EXTXSTREAMINF)
{
AttributesTag *streaminftag = static_cast<AttributesTag *>(lastTag);
/* master playlist uri, merge as attribute */
Attribute *uriAttr = new (std::nothrow) Attribute("URI", std::string(psz_line));
if(uriAttr)
streaminftag->addAttribute(uriAttr);
}
else /* playlist tag, will take modifiers */
{
Tag *tag = TagFactory::createTagByName("", std::string(psz_line));
if(tag)
entrieslist.push_back(tag);
}
lastTag = NULL;
}
else // drop
{
lastTag = NULL;
}
free(psz_line);
}
return entrieslist;
}