Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Add backend support for HTTP Live Streaming of recordings and videos.
This commit adds several classes, logic in mythtranscode, and mythbackend services API glue to allow mythbackend to generate HTTP Live Streams. These are composed of a .m3u8 playlist listing multiple segments of video contained in MPEG-ts files with libx264 encoded video and libmp3lame encoded MP3 audio. In order to use HTTP Live Streaming, you must configure and compile with --enablelibx264 and --enable-libmp3lame. The /Content backend service has several new calls added to start, stop, query, list, and remove live streams. The following have been added: /Content/AddLiveStream /Content/AddRecordingLiveStream /Content/StopLiveStream /Content/RemoveLiveStream /Content/GetLiveStream /Content/GetLiveStreamList For more info on these, see /Content/wsdl Live Stream files are written to a 'Streaming' Storage Group if one is defined, otherwise they will go in ~/.mythtv/tmp/hls. Two sample .qsp files are included for listing recordings (/samples/livestream_rec.qsp) and Storage Group files (/samples/livestream_sg.qsp). These sample pages can be used to start/stop/list/stream/remove Live Streams. The 'Play' links directly to the .m3u8 file which is playable directly under Safari and via external helper apps in other browsers. The Live Streams generated have also been tested with the JW Player flash HTTP Live Streaming support using their adaptiveProvider.swf provider. NOTE: This bumps the binary API version, so make clean, etc.. NOTE2: The DB schema changes as well to support the new livestream table to track existing streams. Known Issues: - audio and video get out of sync sometimes - there is no automated cleanup process for old streams, it is up to the user to delete old streams via the API. - occasional glitches in video, possibly due to keyframes not occurring in the right location at the start of a segment of video. - audio sample rate argument to AddLiveStream is not honored, this will require resampling audio in some cases. I have code to do this, but it needs to be tested more. - no check is done to verify that --enablelibx264 and --enablelibmp3lame were used. TODO: - not going to list all TODO items, but the basic functionality is included in this patch.
- Loading branch information
Showing
25 changed files
with
3,649 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
function basename(path) { | ||
return path.replace( /\\/g, '/').replace( /.*\//, '' ); | ||
} | ||
|
||
function dirname(path) { | ||
return path.replace( /\\/g, '/').replace( /\/[^\/]*$/, '' );; | ||
} | ||
|
||
function parentDirName(path) { | ||
return basename(dirname(path)); | ||
} | ||
|
||
function setupPageName(path) { | ||
return basename(path).replace( /\\/g, '/').replace( /\.[^\.]$/, '' ); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
<html> | ||
<head> | ||
<title><i18n>HTTP Live Stream Demo2</i18n></title> | ||
</head> | ||
<body> | ||
<script language="JavaScript" type="text/javascript" src="/js/fileutil.js"></script> | ||
<script language="JavaScript" type="text/javascript" src="/js/jquery.min.js"></script> | ||
<script language="JavaScript" type="text/javascript"> | ||
|
||
$(document).ready(function() { | ||
listLiveStreams(); | ||
}); | ||
|
||
function tdCell(data) { | ||
return "<td>" + data + "</td>"; | ||
} | ||
|
||
function emptyCell() { | ||
return "<td> </td>"; | ||
} | ||
|
||
function readableDate(str) { | ||
return str.replace( /T..:..:..$/g, '' ); | ||
} | ||
|
||
function playStreamLink(url) { | ||
return "<a href='" + url + "'>Play</a>"; | ||
return "<a href='" + url.replace(/m3u8/g, 'html') + "'>Play</a>"; | ||
} | ||
|
||
function playStream(url) { | ||
url.replace( /m3u8/g, 'html'); | ||
window.location = url; | ||
} | ||
|
||
function stopStreamLink(id) { | ||
return "<a href='javascript:stopStream(" + id + ");'>Stop</a>"; | ||
} | ||
|
||
function stopStream(id) { | ||
$.getJSON("/Content/StopLiveStream", { Id : id }, function(data) { | ||
listLiveStreams(); | ||
}); | ||
} | ||
|
||
function removeStreamLink(id) { | ||
return "<a href='javascript:void(0);' onClick='removeStream(" + id + ");'>[X]</a>"; | ||
} | ||
|
||
function removeStream(id) { | ||
$.getJSON("/Content/RemoveLiveStream", { Id : id }, function(data) { | ||
listLiveStreams(); | ||
}); | ||
} | ||
|
||
function timedReloadList(seconds) { | ||
setTimeout('listLiveStreams()', seconds * 1000); | ||
} | ||
|
||
function listLiveStreams() { | ||
var ls = $("#LiveStreams"); | ||
|
||
if (ls.html() != "") { | ||
ls.html("<i18n>Loading</i18n>..."); | ||
} | ||
$.getJSON("/Content/GetLiveStreamList", { }, function(data) { | ||
if (data.LiveStreamInfoList.LiveStreamInfos.length == 0) { | ||
ls.html(""); | ||
return; | ||
} | ||
var table = ""; | ||
table += "<table border=1 cellpadding=2 cellspacing=2>"; | ||
table += "<tr><th>Play</th><th>Stop</th>" + | ||
"<th>Filename</th><th>Size</th><th>Status</th><th>%</th><th> </th></tr>"; | ||
$.each(data.LiveStreamInfoList.LiveStreamInfos, function(i, value) { | ||
table += "<tr>" + | ||
tdCell(playStreamLink(value.RelativeURL)); | ||
|
||
if (value.StatusStr == "Running") { | ||
table += tdCell(stopStreamLink(value.Id)); | ||
} else { | ||
table += emptyCell(); | ||
} | ||
|
||
table += | ||
tdCell(basename(value.SourceFile)) + | ||
tdCell(value.Width + "x" + value.Height) + | ||
tdCell(value.StatusStr) + | ||
tdCell(value.PercentComplete + "%") + | ||
tdCell(removeStreamLink(value.Id)) + | ||
"</tr>"; | ||
|
||
if ((value.StatusStr == "Queued") || | ||
(value.StatusStr == "Starting") || | ||
(value.StatusStr == "Stopping")) { | ||
timedReloadList(1); | ||
} | ||
}); | ||
table += "</table>"; | ||
ls.html(table); | ||
}); | ||
} | ||
|
||
function startStream(group, filename) { | ||
$.getJSON("/Content/AddLiveStream", { StorageGroup : group, FileName : filename }, function(data) { | ||
listLiveStreams(); | ||
}); | ||
} | ||
|
||
function addStream() { | ||
var datastr = $("#playform").serialize().replace(/%26/g, '&'); | ||
|
||
$.getJSON("/Content/AddRecordingLiveStream", datastr, function(data) { | ||
listLiveStreams(); | ||
}); | ||
} | ||
|
||
function listFiles() { | ||
var group = $("#rgName").val(); | ||
var filter = $("#rgFilter").val().toLowerCase(); | ||
$("#links").html("<i18n>Loading</i18n>..."); | ||
$.getJSON("/Dvr/GetFilteredRecordedList", { Descending: "1", StartIndex: "1", Count: "2000", TitleRegEx : filter, RecGroup : group }, function(data) { | ||
$("#links").html(""); | ||
var str = "<hr><form id='playform'>"; | ||
str += "Width: <select name='Width'>" + | ||
"<option value='288'>288</option>" + | ||
"<option value='400'>400</option>" + | ||
"<option value='480' selected>480</option>" + | ||
"<option value='640'>640</option>" + | ||
"<option value='800'>800</option>" + | ||
"<option value='960'>960</option>" + | ||
"<option value='1024'>1024</option>" + | ||
"<option value='1280'>1280</option>" + | ||
"</select>"; | ||
str += " "; | ||
str += "Height: <select name='Height'>" + | ||
"<option value='0' selected>Auto</option>" + | ||
"<option value='320'>320</option>" + | ||
"<option value='480'>480</option>" + | ||
"<option value='540'>540</option>" + | ||
"<option value='600'>600</option>" + | ||
"<option value='768'>768</option>" + | ||
"<option value='720'>720</option>" + | ||
"</select><br>"; | ||
str += "Bitrate: <select name='Bitrate'>" + | ||
"<option value='500000'>500k</option>" + | ||
"<option value='600000'>600k</option>" + | ||
"<option value='700000'>700k</option>" + | ||
"<option value='800000' selected>800k</option>" + | ||
"<option value='900000'>900k</option>" + | ||
"<option value='1000000'>1000k</option>" + | ||
"<option value='1500000'>1500k</option>" + | ||
"<option value='2000000'>2000k</option>" + | ||
"<option value='2500000'>2500k</option>" + | ||
"<option value='3000000'>3000k</option>" + | ||
"<option value='3500000'>3500k</option>" + | ||
"</select>"; | ||
str += " "; | ||
str += "Audio: <select name='AudioBitrate'>" + | ||
"<option value='32000'>32k</option>" + | ||
"<option value='64000' selected>64k</option>" + | ||
"<option value='96000'>96k</option>" + | ||
"<option value='128000'>128k</option>" + | ||
"<option value='192000'>192k</option>" + | ||
"</select><br>"; | ||
str += "<input type=button onClick='addStream()' Value='Add Stream'>"; | ||
str += "<hr>"; | ||
|
||
$.each(data.ProgramList.Programs, function(i, value) { | ||
str += | ||
'<input type=radio Name="ChanId" Value="' + value.Channel.ChanId + '&StartTime=' + value.StartTime + '"> ' + | ||
readableDate(value.StartTime) + ' - ' + value.Title + ' - ' + value.SubTitle + '<br>'; | ||
}); | ||
str += "</form>"; | ||
$("#links").html(str); | ||
}); | ||
} | ||
</script> | ||
|
||
<font size=+1><i18n>HTTP Live Streams Demo2</i18n></font> <a href='javascript:void(0);' onClick='listLiveStreams()'><font size=-1>(Refresh)</font></a><br> | ||
<div id='LiveStreams'> | ||
</div> | ||
<br> | ||
<div> | ||
<table border=0 cellspacing=2 cellpadding=2> | ||
<i18n>Recording Group</i18n>: | ||
<select id="rgName"> | ||
<!-- Manually fill in your list of Recording Group names here since we can't get them via the API yet --> | ||
<option value='Default'>Default</option> | ||
<option value='LiveTV'>LiveTV</option> | ||
</select><br> | ||
<i18n>Filter</i18n>: <input id='rgFilter' size=20><br> | ||
<input type='button' onClick='javascript:listFiles()' value='<i18n>List Files</i18n>'><br> | ||
</div> | ||
<br> | ||
<div id="links"></div> | ||
|
||
</body> | ||
</html> | ||
|
Oops, something went wrong.