#include "hls_support.h" #include "langcodes.h" /*LTS*/ #include "stream.h" #include #include namespace HLS{ // TODO: Prefix could be made better // Needed for grouping renditions in master manifest const std::string groupIdPrefix = "vid-"; // max partial fragment duration in s const float partDurationMax = (partDurationMaxMs + 4) / 1000.0; // TODO: Advance Part Limit /*If the _HLS_msn is greater than the Media Sequence Number of the last Media Segment in the current Playlist plus two, or if the _HLS_partIf the _HLS_msn is greater than the Media Sequence Number of the last Media Segment in the current Playlist plus two, or if the _HLS_part exceeds the last Partial Segment in the current Playlist by the Advance Part Limit, then the server SHOULD immediately return Bad Request, such as HTTP 400 exceeds the last Partial Segment in the current Playlist by the Advance Part Limit, then the server SHOULD immediately return Bad Request, such as HTTP 400*/ // NOTE: Not implementing this gives freedom to play with jitter duration const uint32_t advancePartLimit = std::min((int)std::ceil(3.0 / partDurationMax), 3); // Ref: RFC8216, 6.2.5.2 const struct{ bool pdu; ///< Playlist Delta Update bool pduV2; ///< TODO: Playlist Delta Update v2: skips EXT-X-DATERANGE bool bpr; ///< Blocking Playlist Reload bool parts; ///< Partial Fragments bool tags; ///< True if any of the above is true }serverSupport ={ #ifdef NOLLHLS false, false, false, false, false, #else true, false, true, true, true, #endif }; /// lastms calculation incorporating jitter duration /// Always ensures the (lastms <= current time - jitter duration) uint64_t getLastms(const DTSC::Meta &M, const std::map &userSelect, const size_t trackIdx, const uint64_t streamStartTime){ std::map::const_iterator it = userSelect.begin(); uint64_t maxJitter = 0; u_int64_t minKeepAway = 0; for (; it != userSelect.end(); it++){ minKeepAway = M.getMinKeepAway(it->first); if (minKeepAway > maxJitter){maxJitter = minKeepAway;} } return std::min(M.getLastms(trackIdx), Util::unixMS() - streamStartTime - minKeepAway); } /// Calculate HLS media playlist version compatibility /// \return version number uint16_t calcManifestVersion(const std::string &hlsSkip){ // Server and Client support skipping media segments if (serverSupport.tags && serverSupport.pdu && (hlsSkip.compare("YES") == 0)){return 9;} // Server and Client support skipping ext-x-daterange along with media segments if (serverSupport.tags && serverSupport.pduV2 && (hlsSkip.compare("v2") == 0)){return 10;} // Default, lowest version supported return 6; } /// returns the main track id provided in master manifest if valid /// else returns the current valid main track id size_t getTimingTrackId(const DTSC::Meta &M, const std::string &mTrack, const size_t mSelTrack){ return (mTrack.size() && (M.getValidTracks().count(atoll(mTrack.c_str())))) ? atoll(mTrack.c_str()) : mSelTrack; } /// Return live edge fragment duration uint64_t getLastFragDur(const DTSC::Meta &M, const std::map &userSelect, const TrackData &trackData, const uint64_t hlsMsnNr, const DTSC::Fragments &fragments, const DTSC::Keys &keys){ return std::min( getLastms(M, userSelect, trackData.timingTrackId, trackData.systemBoot + trackData.bootMsOffset), getLastms(M, userSelect, trackData.requestTrackId, trackData.systemBoot + trackData.bootMsOffset)) - keys.getTime(fragments.getFirstKey(hlsMsnNr)); } /// Waits until the requested fragment & partial fragment are available /// Returns 400 if specific part is requested without a specific MSN /// Returns 400 if requested MSN > the real live edge MSN plus two /// Returns 503 if time spent in BPR > 3x Target Duration uint32_t blockPlaylistReload(const DTSC::Meta &M, const std::map &userSelect, const TrackData &trackData, const HlsSpecData &hlsSpecData, const DTSC::Fragments &fragments, const DTSC::Keys &keys){ // Return if forced noLLHLS if (trackData.noLLHLS){return 0;} // Check BPR request validity if (hlsSpecData.hlsMsn.empty() && hlsSpecData.hlsPart.size()){return 400;} if (atol(hlsSpecData.hlsMsn.c_str()) > (fragments.getEndValid() - 1 + 2)){return 400;} // BPR logic only if live & _HLS_msn requested if (trackData.isLive && hlsSpecData.hlsMsn.size()){ DEBUG_MSG(5, "Requesting media playlist: Track %zu, MSN %s, part: %s", trackData.timingTrackId, hlsSpecData.hlsMsn.c_str(), hlsSpecData.hlsPart.c_str()); uint64_t hlsMsnNr = atol(hlsSpecData.hlsMsn.c_str()); uint64_t hlsPartNr = atol(hlsSpecData.hlsPart.c_str()) + 1; // base 1 // if hlsPart empty (HLS spec) OR if fragment hlsMsn is complete // THEN request part 1 of MSN++ if (hlsSpecData.hlsPart.empty()){hlsPartNr = 1;} if (fragments.getDuration(hlsMsnNr)){ hlsMsnNr++; hlsPartNr = 1; } uint64_t lastFragmentDur = getLastFragDur(M, userSelect, trackData, hlsMsnNr, fragments, keys); std::ldiv_t res = std::ldiv(lastFragmentDur, partDurationMaxMs); DEBUG_MSG(5, "req MSN %" PRIu64 " fin MSN %zu, req Part %" PRIu64 " fin Part %zu", hlsMsnNr, (fragments.getEndValid() - 2), hlsPartNr, res.quot); // BPR Time limit = 3x Target Duration (per HLS spec) // + Jitter duration (per Mist feature) // + 1x Target Duration (extra margin of safety for jitters) int64_t bprTimeLimit = (4 * trackData.targetDurationMax * 1000) + std::max(M.getMinKeepAway(trackData.timingTrackId), M.getMinKeepAway(trackData.requestTrackId)); while (hlsPartNr > res.quot){ if (bprTimeLimit < 1){return 503;} DEBUG_MSG(5, "Part Block: req %" PRIu64 " fin %ld", hlsPartNr, res.quot); Util::wait(partDurationMaxMs - res.rem + 25); bprTimeLimit -= (partDurationMaxMs - res.rem + 25); lastFragmentDur = getLastFragDur(M, userSelect, trackData, hlsMsnNr, fragments, keys); res = std::ldiv(lastFragmentDur, partDurationMaxMs); } } return 0; } /// Populate FragmentData struct to be used for media manifest generation void populateFragmentData(const DTSC::Meta &M, const std::map &userSelect, FragmentData &fragData, const TrackData &trackData, const DTSC::Fragments &fragments, const DTSC::Keys &keys){ fragData.lastMs = std::min( getLastms(M, userSelect, trackData.requestTrackId, trackData.systemBoot + trackData.bootMsOffset), getLastms(M, userSelect, trackData.timingTrackId, trackData.systemBoot + trackData.bootMsOffset)); fragData.firstFrag = fragments.getFirstValid(); if (trackData.isLive){ fragData.lastFrag = M.getFragmentIndexForTime(trackData.timingTrackId, fragData.lastMs); if (fragments.getEndValid() > fragData.lastFrag){ fragData.lastFrag = fragments.getEndValid(); } }else{ // Override to last fragment if VOD fragData.lastFrag = fragments.getEndValid() - 1; } fragData.currentFrag = fragData.firstFrag; fragData.startTime = keys.getTime(fragments.getFirstKey(fragData.currentFrag)); fragData.duration = fragments.getDuration(fragData.currentFrag); // Playlist length limit logic: // Part 1: Limit any playlist with listlimit config if (trackData.listLimit && (fragData.lastFrag - fragData.currentFrag > trackData.listLimit + 2)){ fragData.currentFrag = fragData.lastFrag - trackData.listLimit; } // Part 2: Limit a playlist depending on initial MSN data // see the NOTE at HLS::getLiveLengthLimit(args) if (trackData.isLive && (fragData.lastFrag - fragData.currentFrag) > 2){ fragData.currentFrag = std::max(trackData.initMsn, fragData.currentFrag + 2); } } /// Encryption logic to LLHLS playlist void hlsManifestMediaEncriptionTags(const DTSC::Meta &M, std::stringstream &result, const size_t timingTid){ if (M.getEncryption(timingTid) == ""){ result << "\r\n#EXT-X-KEY:METHOD=NONE"; }else{ // NOTE: // Defined encryption methods: NONE, AES-128, and SAMPLE-AES std::string method = M.getEncryption(timingTid); std::string uri = "asd"; result << "\r\n#EXT-X-KEY:METHOD=" << method; result << ",URI=\"" << uri << "\""; // if (version >= 5){ // result << "\",KEYFORMAT=\"com.apple.streamingkeydelivery\""; // result << ""; //} } } void addMsnTag(std::stringstream &result, const uint64_t msn){ result << "#EXT-X-MEDIA-SEQUENCE:" << msn << "\r\n"; } /// Returns skip boundary duration, calculated as 6x max target duration uint32_t hlsSkipBoundary(const uint32_t targetDurationMax){return targetDurationMax * 6;} /// Calculates the number full fragments that can be skipped from printing in the manifest /// and MUST REPLACE the associated tags void addMediaSkipTag(std::stringstream &result, FragmentData &fragData, const TrackData &trackData, const uint16_t version){ // NOTE: Skips supported from version >= 9 // Version >=9 supports SKIPPED-SEGMENTS // TODO: Support for Version 10 playlists // NOTE: Not implemented only because there is no immediate demand from anyone. // Adds support for RECENTLY-REMOVED-DATERANGES if (version >= 9){ uint32_t skips = 0; const uint32_t skipsFromEnd = hlsSkipBoundary(trackData.targetDurationMax) / trackData.targetDurationMax + 2; if ((fragData.lastFrag - fragData.currentFrag) > skipsFromEnd){ skips = ((fragData.lastFrag - fragData.currentFrag) - skipsFromEnd); } if (version >= 10){ // TODO: Implement logic for skip calculations date ranges skips += 0; } if (skips){ result << "#EXT-X-SKIP:SKIPPED-SEGMENTS=" << skips << "\r\n"; // TODO: Update with version 10 playlist implementation // result << ",RECENTLY-REMOVED-DATERANGES="; fragData.currentFrag += skips; } } } /// Append result with tags that indicates the server supports LLHLS delivery void addServerSupportTags(std::stringstream &result, const TrackData &trackData){ if (trackData.noLLHLS || !trackData.isLive){return;} // TODO: Make ifdef if (serverSupport.tags){ result << "#EXT-X-SERVER-CONTROL:"; if (serverSupport.bpr){result << "CAN-BLOCK-RELOAD=YES,";} if (serverSupport.pdu){ result << "CAN-SKIP-UNTIL=" << hlsSkipBoundary(trackData.targetDurationMax) << ","; } if (serverSupport.pduV2){result << "CAN-SKIP-DATERANGES=YES,";} if (serverSupport.parts){ result << "PART-HOLD-BACK=" << partDurationMax * 3; // atleast 3x result << "\r\n#EXT-X-PART-INF:PART-TARGET=" << partDurationMax; } result << "\r\n"; } } void addTargetDuration(std::stringstream &result, const uint32_t targetDurationMax){ result << "#EXT-X-TARGETDURATION:" << targetDurationMax << "\r\n"; } /// Appends result with encrytion / drm data void addEncriptionTags(std::stringstream &result, const std::string &encryptMethod){ // TODO: Add support for media encryption if (encryptMethod.size()){ // NOTE: // Defined encryption methods: NONE, AES-128, and SAMPLE-AES std::string uri = "asd"; result << "#EXT-X-KEY:METHOD=" << encryptMethod; result << ",URI=\"" << uri << "\"\r\n"; // if (version >= 5){ // result << "\",KEYFORMAT=\"com.apple.streamingkeydelivery\""; // result << ""; //} } } void addInitTags(std::stringstream &result, const TrackData &trackData){ // No init data for TS if (trackData.mediaFormat == ".ts"){return;} result << "#EXT-X-MAP:URI=\"" << trackData.urlPrefix << "init" << trackData.mediaFormat; if (trackData.sessionId.size()){result << "?sessId=" << trackData.sessionId;} result << "\"\r\n"; } void addMediaBasicTags(std::stringstream &result, const uint16_t version){ result << "#EXTM3U\r\n"; result << "#EXT-X-VERSION:" << version << "\r\n"; } /// Append result with media meta tags that are in the beginning of the manifest void addStartingMetaTags(std::stringstream &result, FragmentData &fragData, const TrackData &trackData, const HlsSpecData &hlsSpecData){ const uint16_t version = calcManifestVersion(hlsSpecData.hlsSkip); addMediaBasicTags(result, version); addServerSupportTags(result, trackData); addInitTags(result, trackData); addEncriptionTags(result, trackData.encryptMethod); addTargetDuration(result, trackData.targetDurationMax); addMsnTag(result, trackData.isLive ? fragData.currentFrag : fragData.firstFrag); // NOTE: DO NOT move the SKIP tag. Order must be respected per HLS spec. addMediaSkipTag(result, fragData, trackData, version); } /// Appends result with prependStr and timestamp calculated from current time in ms void addDateTimeTag(std::stringstream &result, const std::string &prependStr, const uint64_t unixMs){ time_t uSecs = unixMs / 1000; struct tm *ptm = gmtime(&uSecs); char dt_iso_8601[25]; snprintf(dt_iso_8601, 25, "%.4d-%.2d-%.2dT%.2d:%.2d:%.2d.%.3dZ", ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday, ptm->tm_hour, ptm->tm_min, ptm->tm_sec, (int)(unixMs % 1000)); result << prependStr << dt_iso_8601 << "\r\n"; } /// Add segment tag to LLHLS playlist void addFragmentTag(std::stringstream &result, const FragmentData &fragData, const TrackData &trackData){ result << "#EXTINF:" << std::fixed << std::setprecision(3) << fragData.duration / 1000.0 << ",\r\n"; // NOTE: HLS spec says it isn't mandatory to add date time tag for every fragment. // Tests show that there is definitely an influence on consistency for live streams. // Printing the tag for every fragment tag was the best. if (trackData.isLive){ addDateTimeTag(result, "#EXT-X-PROGRAM-DATE-TIME:", trackData.systemBoot + trackData.bootMsOffset + fragData.startTime); } result << trackData.urlPrefix << "chunk_" << fragData.startTime << trackData.mediaFormat; result << "?msn=" << fragData.currentFrag; result << "&mTrack=" << trackData.timingTrackId; result << "&dur=" << fragData.duration; if (trackData.sessionId.size()){result << "&sessId=" << trackData.sessionId;} result << "\r\n"; } /// Add partial segment tag to LLHLS playlist void addPartialTag(std::stringstream &result, const DTSC::Meta &M, const DTSC::Keys &keys, const FragmentData &fragData, const TrackData &trackData, const uint32_t partCount, const uint32_t duration){ result << "#EXT-X-PART:DURATION=" << duration / 1000.0; result << ",URI=\"" << trackData.urlPrefix; result << "chunk_" << fragData.startTime << "." << partCount << trackData.mediaFormat; result << "?msn=" << fragData.currentFrag; result << "&mTrack=" << trackData.timingTrackId; result << "&dur=" << duration; if (trackData.sessionId.size()){result << "&sessId=" << trackData.sessionId;} result << "\""; // NOTE: INDEPENDENT tags, specified ONLY for VIDEO tracks, indicate the first partial fragment // closest to the before (live edge - PART-HOLD-BACK) time that a client starts playback from. if (trackData.isVideo){ uint64_t partStartTime = fragData.startTime + partCount * partDurationMaxMs; uint32_t partKeyIdx = M.getKeyIndexForTime(trackData.timingTrackId, partStartTime); uint64_t partKeyIdxTime = M.getTimeForKeyIndex(trackData.timingTrackId, partKeyIdx); if (partKeyIdxTime == partStartTime){result << ",INDEPENDENT=YES";} } result << "\r\n"; } /// Appends result with partial fragment tags if supported/requested void addPartialFragmentTags(std::stringstream &result, const DTSC::Meta &M, FragmentData &fragData, const TrackData &trackData, const DTSC::Keys &keys){ if (trackData.noLLHLS){return;} // return if VOD, or no support for server tags, or no support for partial fragments if (!(trackData.isLive && serverSupport.tags && serverSupport.parts)){return;} // if fragment is last-but-4th or later // OR if fragment is 3 target durations from the end if ((fragData.lastFrag - fragData.currentFrag < 5) || ((fragData.lastMs - fragData.startTime) <= 3 * trackData.targetDurationMax * 1000)){ std::ldiv_t durationData = std::ldiv(fragData.duration, partDurationMaxMs); // General case: all partial segments with duration equal to partDurationMax uint32_t partCount = 0; for (partCount = 0; partCount < durationData.quot; partCount++){ addPartialTag(result, M, keys, fragData, trackData, partCount, partDurationMaxMs); } // Special case: last partial segment (duration < partDurationMaxMs) in any fragment not at // live edge if (durationData.rem && (fragData.lastFrag - fragData.currentFrag > 1)){ addPartialTag(result, M, keys, fragData, trackData, partCount, durationData.rem); } fragData.partNum = partCount; } } /// Appends result with partial fragment tags, date-time tag and fragment tag for the current /// fragment void addMediaTags(std::stringstream &result, const DTSC::Meta &M, FragmentData &fragData, const TrackData &trackData, const DTSC::Keys &keys){ addPartialFragmentTags(result, M, fragData, trackData, keys); // do not add the last fragment media tag for the live streams if (trackData.isLive && (fragData.currentFrag == fragData.lastFrag - 1)){return;} addFragmentTag(result, fragData, trackData); } /// Appends result with partial fragment tags, date-time tags and fragment tags for all fragments void addMediaFragments(std::stringstream &result, const DTSC::Meta &M, FragmentData &fragData, const TrackData &trackData, const DTSC::Fragments &fragments, const DTSC::Keys &keys){ for (; fragData.currentFrag < fragData.lastFrag; fragData.currentFrag++){ fragData.startTime = keys.getTime(fragments.getFirstKey(fragData.currentFrag)); // adjust fragment start time for vod if (!trackData.isLive){fragData.startTime -= M.getFirstms(trackData.timingTrackId);} fragData.duration = fragments.getDuration(fragData.currentFrag); // NOTE: If duration invalid, it's the last fragment, so calculate duration from live edge // Needed for LLHLS if (!fragData.duration){fragData.duration = fragData.lastMs - fragData.startTime;} addMediaTags(result, M, fragData, trackData, keys); } } void addVodEndingTags(std::stringstream &result){result << "#EXT-X-ENDLIST\r\n";} /// Append result with information on alternate renditions, only for LLHLS void addAltRenditionReports(std::stringstream &result, const DTSC::Meta &M, const std::map &userSelect, const FragmentData &fragData, const TrackData &trackData){ DTSC::Fragments fragments(M.fragments(trackData.timingTrackId)); std::ldiv_t altPart = std::ldiv(fragments.getDuration(fragData.currentFrag - 2), partDurationMaxMs); std::map::const_iterator it = userSelect.end(); for (; it != userSelect.end(); it++){ if (it->first == trackData.timingTrackId){continue;} result << "#EXT-X-RENDITION-REPORT:"; result << "URI=\"" << it->first << "/index.m3u8\""; if (fragData.partNum){ result << ",LAST-MSN=" << fragData.currentFrag - 1; result << ",LAST-PART=" << fragData.partNum - 1 << "\r\n"; }else{ result << ",LAST-MSN=" << fragData.currentFrag - 2; result << ",LAST-PART=" << ((altPart.quot - 1) + (altPart.rem ? 1 : 0)) << "\r\n"; } } } /// Append result with hinted part, only for LLHLS void addPreloadHintTag(std::stringstream &result, const FragmentData &fragData, const TrackData &trackData){ result << "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"" << trackData.urlPrefix << "chunk_"; result << fragData.startTime << "." << fragData.partNum << trackData.mediaFormat; result << "?msn=" << fragData.currentFrag - 1; result << "&mTrack=" << trackData.timingTrackId; result << "&dur=" << partDurationMaxMs; if (trackData.sessionId.size()){result << "&sessId=" << trackData.sessionId;} result << "\"\r\n"; } /// Append result with live ending tags (supports llhls tags) void addLiveEndingTags(std::stringstream &result, const DTSC::Meta &M, const std::map &userSelect, const FragmentData &fragData, const TrackData &trackData){ if (trackData.noLLHLS){return;} if (serverSupport.tags && serverSupport.parts){ addPreloadHintTag(result, fragData, trackData); addAltRenditionReports(result, M, userSelect, fragData, trackData); } } /// Add respective ending tags for VOD and Live streams void addEndingTags(std::stringstream &result, const DTSC::Meta &M, const std::map &userSelect, const FragmentData &fragData, const TrackData &trackData){ trackData.isLive ? addLiveEndingTags(result, M, userSelect, fragData, trackData) : addVodEndingTags(result); } /// Check if keyframes of given trackId are aligned with that of the main track /// returns true if aligned /// keyframe alginment is a MUST for LLHLS track switch bool checkFramesAlignment(std::stringstream &result, const DTSC::Meta &M, const MasterData &masterData, const size_t trackId){ bool keyFramesAligned = masterData.mainTrack == trackId || M.keyTimingsMatch(masterData.mainTrack, trackId); if (!keyFramesAligned){ result << "## NOTE: Track " << trackId << " is available, but ignored because it is not aligned with track " << masterData.mainTrack << ".\r\n"; } return keyFramesAligned; } /// Adds EXT-X-MEDIA tag for a given trackId void addExtXMediaTags(std::stringstream &result, const DTSC::Meta &M, const MasterData &masterData, const size_t trackId, const std::string &mediaType, const std::string &grpid, const uint64_t iFrag){ std::string lang = ""; lang = M.getLang(trackId).empty() ? "und" : M.getLang(trackId); std::string name = M.getCodec(trackId) + "-"; if (lang == "und"){ char intStr[10]; snprintf(intStr, 10, "%zu", trackId); name += intStr; }else{ name += lang; } result << "#EXT-X-MEDIA:TYPE=" << mediaType; result << ",GROUP-ID=\"" << grpid << "\""; result << ",LANGUAGE=\"" << lang; if (lang == "und"){result << "-" << trackId;} result << "\""; result << ",NAME=\"" << name << "\",URI=\"" << trackId << "/index.m3u8"; result << "?mTrack=" << masterData.mainTrack; result << "&iMsn=" << iFrag; if (masterData.hasSessId){result << "&sessId=" << masterData.sessId;} if (masterData.noLLHLS){result << "&llhls=0";} result << "\"\r\n"; } /// Add HLS basic tags for master manifest void addMasterBasicTags(std::stringstream &result){ result.str(std::string()); // reset the stream to empty result << "#EXTM3U\r\n#EXT-X-INDEPENDENT-SEGMENTS\r\n"; } void addInfTrackTag(std::stringstream &result, const MasterData &masterData, const std::set &aTracks, const size_t tid, const uint64_t iFrag, const bool keyFramesAligned, const bool isVideo){ result << (keyFramesAligned ? "" : "## DISABLED: "); result << tid; if (isVideo && masterData.isTS && aTracks.size() == 1){result << "_" << *aTracks.begin();} result << "/index.m3u8"; result << "?mTrack=" << masterData.mainTrack; result << "&iMsn=" << iFrag; if (masterData.hasSessId){result << "&sessId=" << masterData.sessId;} if (masterData.noLLHLS){result << "&llhls=0";} result << "\r\n"; } void addInfBWidthTag(std::stringstream &result, const uint64_t bWidth){ result << std::fixed << std::setprecision(0); result << ",BANDWIDTH=" << bWidth * 1.3 << ",AVERAGE-BANDWIDTH=" << bWidth * 1.1 << "\r\n"; } void addInfResolFrameRate(std::stringstream &result, const DTSC::Meta &M, const std::string &resolution, const size_t trackId){ result << ",RESOLUTION=" << resolution; if (M.getFpks(trackId)){result << ",FRAME-RATE=" << (float)M.getFpks(trackId) / 1000;} } void addInfCodecsTag(std::stringstream &result, const DTSC::Meta &M, const size_t tid, const std::string &audCodecsStr){ result << "CODECS=\"" << Util::codecString(M.getCodec(tid), M.getInit(tid)); result << audCodecsStr << "\""; } /// creates group id based on the resolution of the track void getGroupId(std::stringstream &grpid, const DTSC::Meta &M, const size_t tid){ grpid.str(std::string()); // reset the stream to empty grpid << groupIdPrefix << M.getWidth(tid) << "x" << M.getHeight(tid); } void addInfMainTag(std::stringstream &result){result << "#EXT-X-STREAM-INF:";} /// add #EXT-X-STREAM-INF for audio only streams void addAudInfStreamTags(std::stringstream &result, const DTSC::Meta &M, const MasterData &masterData, const std::set &aTracks, const uint64_t iFrag){ if (aTracks.size()){ for (std::set::iterator ita = aTracks.begin(); ita != aTracks.end(); ita++){ uint64_t bWidth = M.getBps(*ita); bWidth = (bWidth < 5 ? 5 : bWidth) * 8; addInfMainTag(result); addInfCodecsTag(result, M, *ita, ""); addInfBWidthTag(result, bWidth); addInfTrackTag(result, masterData, aTracks, *ita, iFrag, true, false); } } } /// Add #EXT-X-STREAM-INF tags for video groups void addVidInfStreamTags(std::stringstream &result, const DTSC::Meta &M, const MasterData &masterData, const std::set &aCodecs, const std::set > &vTracks, const std::set &aTracks, const std::multimap &vidGroups, const uint64_t asBWidth, const uint64_t iFrag, const uint32_t sTracksSize){ // Create a comma separated string containing all audio codecs std::string audCodecsStr = ""; // comma separated string of "audioCodecs" if (aCodecs.size()){ for (std::set::iterator it = aCodecs.begin(); it != aCodecs.end(); ++it){ audCodecsStr += ","; audCodecsStr += *it; } } std::string assocGroupTag = ""; // add associate group tags if ((!masterData.isTS && aTracks.size()) || (masterData.isTS && aTracks.size() > 1)){ assocGroupTag += "AUDIO=\"aud\","; } if (sTracksSize){assocGroupTag += "SUBTITLES=\"sub\",";} for (std::set::iterator itr = vTracks.begin(); itr != vTracks.end(); itr++){ std::map::const_iterator it = vidGroups.begin(); while (it != vidGroups.end()){ if (*itr == it->second){break;} it++; } if (it == vidGroups.end()){continue;} bool keyFramesAligned = checkFramesAlignment(result, M, masterData, it->second); if (keyFramesAligned){ uint64_t bWidth = M.getBps(it->second); bWidth = ((bWidth < 5 ? 5 : bWidth) + asBWidth) * 8; addInfMainTag(result); result << assocGroupTag; addInfCodecsTag(result, M, it->second, audCodecsStr); addInfResolFrameRate(result, M, it->first.substr(groupIdPrefix.size()), it->second); addInfBWidthTag(result, bWidth); addInfTrackTag(result, masterData, aTracks, it->second, iFrag, keyFramesAligned, true); } } } /// Adds EXT-X-MEDIA:TYPE=SUBTITLES tags to the manifest uint64_t addSubTags(std::stringstream &result, const DTSC::Meta &M, const MasterData &masterData, const std::set &sTracks, const uint64_t iFrag){ uint64_t subBWidth = 0; for (std::set::iterator its = sTracks.begin(); its != sTracks.end(); its++){ addExtXMediaTags(result, M, masterData, *its, "SUBTITLES", "sub", iFrag); subBWidth = std::max(subBWidth, M.getBps(*its)); } return subBWidth; } /// Adds EXT-X-MEDIA:TYPE=AUDIO tags to the manifest uint64_t addAudTags(std::stringstream &result, std::set &aCodecs, const DTSC::Meta &M, const MasterData &masterData, const std::set &aTracks, const uint64_t iFrag, const uint32_t vTracksLength){ // if video tracks available, audio tracks as EXT-X-MEDIA, else as EXT-X-STREAM-INF uint64_t audBWidth = 0; if (vTracksLength){ for (std::set::iterator ita = aTracks.begin(); ita != aTracks.end(); ita++){ if (!masterData.isTS || (masterData.isTS && aTracks.size() > 1)){ addExtXMediaTags(result, M, masterData, *ita, "AUDIO", "aud", iFrag); } aCodecs.insert(Util::codecString(M.getCodec(*ita), M.getInit(*ita))); audBWidth = std::max(audBWidth, M.getBps(*ita)); } } return audBWidth; } /// Adds EXT-X-MEDIA:TYPE=VIDEO tags to the manifest void addVidTags(std::stringstream &result, std::stringstream &grpid, const DTSC::Meta &M, const MasterData &masterData, const std::set > &vTracks, const std::multimap &vidGroups, const uint64_t iFrag, const uint32_t aTracksSize){ // if audio tracks available, video tracks are EXT-X-STREAM-INF std::set::iterator itv = aTracksSize ? vTracks.end() : vTracks.begin(); for (; itv != vTracks.end(); itv++){ getGroupId(grpid, M, *itv); if (vidGroups.find(grpid.str()) != vidGroups.end() && vidGroups.count(grpid.str()) == 1){ continue; } if (checkFramesAlignment(result, M, masterData, *itv)){ addExtXMediaTags(result, M, masterData, *itv, "VIDEO", grpid.str(), iFrag); } } } /// Sorts all tracks into video, audio & subtitle sets & generate rendition groups void sortTracks(const DTSC::Meta &M, const std::map &userSelect, std::stringstream &grpid, std::set > &vTracks, std::set &aTracks, std::set &sTracks, std::multimap &vidGroups){ std::map::const_iterator it = userSelect.begin(); for (; it != userSelect.end(); it++){ if (M.getType(it->first) == "video"){ vTracks.insert(it->first); getGroupId(grpid, M, it->first); vidGroups.insert(std::pair(grpid.str(), it->first)); } if (M.getType(it->first) == "audio"){aTracks.insert(it->first);} if (M.getCodec(it->first) == "subtitle"){sTracks.insert(it->first);} } } /// This is a hack to ensure the LLHLS playback starts as close as possible to the live edge u_int16_t getLiveLengthLimit(const MasterData &masterData){ // NOTE: // TL;DR: Apple cleints receive the shortest media playlist to ensure a consistent playback at // least possible latency. // Long story: After experimentation, it was found that Apple clients start streaming // consistently at least latency when the first playlist is short, i.e., ~1 full fragment (+ // partial fragment if any) short. From that point, the playlist can grow with the stream. // TODO: remove this when the above issue with apple clients is observed no more. return (masterData.userAgent.find(" Mac OS ") != std::string::npos) ? 3 : 6; } /// Get the first fragment number to be printed in the playlist u_int64_t getInitFragment(const DTSC::Meta &M, const MasterData &masterData){ if (M.getLive()){ DTSC::Fragments fragments(M.fragments(masterData.mainTrack)); DTSC::Keys keys(M.keys(masterData.mainTrack)); u_int64_t iFrag = std::max(fragments.getEndValid() - (masterData.noLLHLS ? 10 : getLiveLengthLimit(masterData)), fragments.getFirstValid()); uint64_t minDur = M.getLastms(masterData.mainTrack) - keys.getTime(fragments.getFirstKey(iFrag)); if (minDur < HLS::partDurationMaxMs * 3){iFrag--;} return iFrag; }else{ return 0; } } /// Appends master manifest to result void addMasterManifest(std::stringstream &result, const DTSC::Meta &M, const std::map &userSelect, const MasterData &masterData){ std::set > vTracks; std::set aTracks; std::set sTracks; std::stringstream grpid; ///< used for vidGroups. std::multimap vidGroups; ///< stores 1 video track id from a groupid std::set aCodecs; ///< a set to store unique audio codecs sortTracks(M, userSelect, grpid, vTracks, aTracks, sTracks, vidGroups); const uint64_t iFrag = getInitFragment(M, masterData); addMasterBasicTags(result); addVidTags(result, grpid, M, masterData, vTracks, vidGroups, iFrag, aTracks.size()); uint64_t audBWidth = addAudTags(result, aCodecs, M, masterData, aTracks, iFrag, vTracks.size()); uint64_t subBWidth = addSubTags(result, M, masterData, sTracks, iFrag); if (vidGroups.size()){ addVidInfStreamTags(result, M, masterData, aCodecs, vTracks, aTracks, vidGroups, audBWidth + subBWidth, iFrag, sTracks.size()); }else{ addAudInfStreamTags(result, M, masterData, aTracks, iFrag); } } /// returns the end time for a given partial fragment /// returns 0 for a hinted part which never got created uint64_t getPartTargetTime(const DTSC::Meta &M, const uint32_t idx, const uint32_t mTrack, const uint64_t startTime, const uint64_t msn, const uint32_t part){ DTSC::Fragments fragments(M.fragments(mTrack)); // Estimate the target end time for a given part // 50 ms is margin of safety to accommodate inconsistencies const uint64_t calcTargetTime = startTime + (part + 1) * partDurationMaxMs + 50; uint64_t lastms = std::min(M.getLastms(mTrack), M.getLastms(idx)); uint16_t count = 0; // wait until estimated target end time is <= lastms for the track while (calcTargetTime > lastms && count++ < 50){ Util::wait(calcTargetTime - lastms); lastms = std::min(M.getLastms(mTrack), M.getLastms(idx)); } // Duration maybe invalid, indicating msn is not complete // But the part is ready. So return the end time uint64_t duration = fragments.getDuration(msn); if (!duration){return startTime + ((part + 1) * partDurationMaxMs);} // If duration valid, MSN is fully finished // Possible that the last partial fragment duration < partDurationMaxMs // Find the exact duration of the last partial fragment uint64_t partTargetTime = std::min(startTime + duration, startTime + ((part + 1) * partDurationMaxMs)); if (duration && (partTargetTime - startTime) > duration){return 0;} return partTargetTime; } }// namespace HLS