Skip to content

Commit

Permalink
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
cpinkham committed Dec 1, 2011
1 parent 872161c commit 7e1a770
Show file tree
Hide file tree
Showing 25 changed files with 3,649 additions and 67 deletions.
2 changes: 1 addition & 1 deletion mythtv/bindings/perl/MythTV.pm
Expand Up @@ -114,7 +114,7 @@ package MythTV;
# schema version supported in the main code. We need to check that the schema
# version in the database is as expected by the bindings, which are expected
# to be kept in sync with the main code.
our $SCHEMA_VERSION = "1287";
our $SCHEMA_VERSION = "1288";

# NUMPROGRAMLINES is defined in mythtv/libs/libmythtv/programinfo.h and is
# the number of items in a ProgramInfo QStringList group used by
Expand Down
2 changes: 1 addition & 1 deletion mythtv/bindings/python/MythTV/static.py
Expand Up @@ -5,7 +5,7 @@
"""

OWN_VERSION = (0,25,-1,3)
SCHEMA_VERSION = 1287
SCHEMA_VERSION = 1288
NVSCHEMA_VERSION = 1007
MUSICSCHEMA_VERSION = 1018
PROTO_VERSION = '70'
Expand Down
16 changes: 16 additions & 0 deletions mythtv/html/js/fileutil.js
@@ -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( /\.[^\.]$/, '' );
}

200 changes: 200 additions & 0 deletions mythtv/html/samples/livestream_rec.qsp
@@ -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>&nbsp;</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>&nbsp;</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 += "&nbsp;&nbsp;&nbsp;";
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 += "&nbsp;&nbsp;&nbsp;";
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 + '">&nbsp;' +
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>

0 comments on commit 7e1a770

Please sign in to comment.