Skip to content

Commit 7e1a770

Browse files
committed
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.
1 parent 872161c commit 7e1a770

File tree

25 files changed

+3649
-67
lines changed

25 files changed

+3649
-67
lines changed

mythtv/bindings/perl/MythTV.pm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ package MythTV;
114114
# schema version supported in the main code. We need to check that the schema
115115
# version in the database is as expected by the bindings, which are expected
116116
# to be kept in sync with the main code.
117-
our $SCHEMA_VERSION = "1287";
117+
our $SCHEMA_VERSION = "1288";
118118

119119
# NUMPROGRAMLINES is defined in mythtv/libs/libmythtv/programinfo.h and is
120120
# the number of items in a ProgramInfo QStringList group used by

mythtv/bindings/python/MythTV/static.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66

77
OWN_VERSION = (0,25,-1,3)
8-
SCHEMA_VERSION = 1287
8+
SCHEMA_VERSION = 1288
99
NVSCHEMA_VERSION = 1007
1010
MUSICSCHEMA_VERSION = 1018
1111
PROTO_VERSION = '70'

mythtv/html/js/fileutil.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
function basename(path) {
2+
return path.replace( /\\/g, '/').replace( /.*\//, '' );
3+
}
4+
5+
function dirname(path) {
6+
return path.replace( /\\/g, '/').replace( /\/[^\/]*$/, '' );;
7+
}
8+
9+
function parentDirName(path) {
10+
return basename(dirname(path));
11+
}
12+
13+
function setupPageName(path) {
14+
return basename(path).replace( /\\/g, '/').replace( /\.[^\.]$/, '' );
15+
}
16+
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
<html>
2+
<head>
3+
<title><i18n>HTTP Live Stream Demo2</i18n></title>
4+
</head>
5+
<body>
6+
<script language="JavaScript" type="text/javascript" src="/js/fileutil.js"></script>
7+
<script language="JavaScript" type="text/javascript" src="/js/jquery.min.js"></script>
8+
<script language="JavaScript" type="text/javascript">
9+
10+
$(document).ready(function() {
11+
listLiveStreams();
12+
});
13+
14+
function tdCell(data) {
15+
return "<td>" + data + "</td>";
16+
}
17+
18+
function emptyCell() {
19+
return "<td>&nbsp;</td>";
20+
}
21+
22+
function readableDate(str) {
23+
return str.replace( /T..:..:..$/g, '' );
24+
}
25+
26+
function playStreamLink(url) {
27+
return "<a href='" + url + "'>Play</a>";
28+
return "<a href='" + url.replace(/m3u8/g, 'html') + "'>Play</a>";
29+
}
30+
31+
function playStream(url) {
32+
url.replace( /m3u8/g, 'html');
33+
window.location = url;
34+
}
35+
36+
function stopStreamLink(id) {
37+
return "<a href='javascript:stopStream(" + id + ");'>Stop</a>";
38+
}
39+
40+
function stopStream(id) {
41+
$.getJSON("/Content/StopLiveStream", { Id : id }, function(data) {
42+
listLiveStreams();
43+
});
44+
}
45+
46+
function removeStreamLink(id) {
47+
return "<a href='javascript:void(0);' onClick='removeStream(" + id + ");'>[X]</a>";
48+
}
49+
50+
function removeStream(id) {
51+
$.getJSON("/Content/RemoveLiveStream", { Id : id }, function(data) {
52+
listLiveStreams();
53+
});
54+
}
55+
56+
function timedReloadList(seconds) {
57+
setTimeout('listLiveStreams()', seconds * 1000);
58+
}
59+
60+
function listLiveStreams() {
61+
var ls = $("#LiveStreams");
62+
63+
if (ls.html() != "") {
64+
ls.html("<i18n>Loading</i18n>...");
65+
}
66+
$.getJSON("/Content/GetLiveStreamList", { }, function(data) {
67+
if (data.LiveStreamInfoList.LiveStreamInfos.length == 0) {
68+
ls.html("");
69+
return;
70+
}
71+
var table = "";
72+
table += "<table border=1 cellpadding=2 cellspacing=2>";
73+
table += "<tr><th>Play</th><th>Stop</th>" +
74+
"<th>Filename</th><th>Size</th><th>Status</th><th>%</th><th>&nbsp;</th></tr>";
75+
$.each(data.LiveStreamInfoList.LiveStreamInfos, function(i, value) {
76+
table += "<tr>" +
77+
tdCell(playStreamLink(value.RelativeURL));
78+
79+
if (value.StatusStr == "Running") {
80+
table += tdCell(stopStreamLink(value.Id));
81+
} else {
82+
table += emptyCell();
83+
}
84+
85+
table +=
86+
tdCell(basename(value.SourceFile)) +
87+
tdCell(value.Width + "x" + value.Height) +
88+
tdCell(value.StatusStr) +
89+
tdCell(value.PercentComplete + "%") +
90+
tdCell(removeStreamLink(value.Id)) +
91+
"</tr>";
92+
93+
if ((value.StatusStr == "Queued") ||
94+
(value.StatusStr == "Starting") ||
95+
(value.StatusStr == "Stopping")) {
96+
timedReloadList(1);
97+
}
98+
});
99+
table += "</table>";
100+
ls.html(table);
101+
});
102+
}
103+
104+
function startStream(group, filename) {
105+
$.getJSON("/Content/AddLiveStream", { StorageGroup : group, FileName : filename }, function(data) {
106+
listLiveStreams();
107+
});
108+
}
109+
110+
function addStream() {
111+
var datastr = $("#playform").serialize().replace(/%26/g, '&');
112+
113+
$.getJSON("/Content/AddRecordingLiveStream", datastr, function(data) {
114+
listLiveStreams();
115+
});
116+
}
117+
118+
function listFiles() {
119+
var group = $("#rgName").val();
120+
var filter = $("#rgFilter").val().toLowerCase();
121+
$("#links").html("<i18n>Loading</i18n>...");
122+
$.getJSON("/Dvr/GetFilteredRecordedList", { Descending: "1", StartIndex: "1", Count: "2000", TitleRegEx : filter, RecGroup : group }, function(data) {
123+
$("#links").html("");
124+
var str = "<hr><form id='playform'>";
125+
str += "Width: <select name='Width'>" +
126+
"<option value='288'>288</option>" +
127+
"<option value='400'>400</option>" +
128+
"<option value='480' selected>480</option>" +
129+
"<option value='640'>640</option>" +
130+
"<option value='800'>800</option>" +
131+
"<option value='960'>960</option>" +
132+
"<option value='1024'>1024</option>" +
133+
"<option value='1280'>1280</option>" +
134+
"</select>";
135+
str += "&nbsp;&nbsp;&nbsp;";
136+
str += "Height: <select name='Height'>" +
137+
"<option value='0' selected>Auto</option>" +
138+
"<option value='320'>320</option>" +
139+
"<option value='480'>480</option>" +
140+
"<option value='540'>540</option>" +
141+
"<option value='600'>600</option>" +
142+
"<option value='768'>768</option>" +
143+
"<option value='720'>720</option>" +
144+
"</select><br>";
145+
str += "Bitrate: <select name='Bitrate'>" +
146+
"<option value='500000'>500k</option>" +
147+
"<option value='600000'>600k</option>" +
148+
"<option value='700000'>700k</option>" +
149+
"<option value='800000' selected>800k</option>" +
150+
"<option value='900000'>900k</option>" +
151+
"<option value='1000000'>1000k</option>" +
152+
"<option value='1500000'>1500k</option>" +
153+
"<option value='2000000'>2000k</option>" +
154+
"<option value='2500000'>2500k</option>" +
155+
"<option value='3000000'>3000k</option>" +
156+
"<option value='3500000'>3500k</option>" +
157+
"</select>";
158+
str += "&nbsp;&nbsp;&nbsp;";
159+
str += "Audio: <select name='AudioBitrate'>" +
160+
"<option value='32000'>32k</option>" +
161+
"<option value='64000' selected>64k</option>" +
162+
"<option value='96000'>96k</option>" +
163+
"<option value='128000'>128k</option>" +
164+
"<option value='192000'>192k</option>" +
165+
"</select><br>";
166+
str += "<input type=button onClick='addStream()' Value='Add Stream'>";
167+
str += "<hr>";
168+
169+
$.each(data.ProgramList.Programs, function(i, value) {
170+
str +=
171+
'<input type=radio Name="ChanId" Value="' + value.Channel.ChanId + '&StartTime=' + value.StartTime + '">&nbsp;' +
172+
readableDate(value.StartTime) + ' - ' + value.Title + ' - ' + value.SubTitle + '<br>';
173+
});
174+
str += "</form>";
175+
$("#links").html(str);
176+
});
177+
}
178+
</script>
179+
180+
<font size=+1><i18n>HTTP Live Streams Demo2</i18n></font> <a href='javascript:void(0);' onClick='listLiveStreams()'><font size=-1>(Refresh)</font></a><br>
181+
<div id='LiveStreams'>
182+
</div>
183+
<br>
184+
<div>
185+
<table border=0 cellspacing=2 cellpadding=2>
186+
<i18n>Recording Group</i18n>:
187+
<select id="rgName">
188+
<!-- Manually fill in your list of Recording Group names here since we can't get them via the API yet -->
189+
<option value='Default'>Default</option>
190+
<option value='LiveTV'>LiveTV</option>
191+
</select><br>
192+
<i18n>Filter</i18n>: <input id='rgFilter' size=20><br>
193+
<input type='button' onClick='javascript:listFiles()' value='<i18n>List Files</i18n>'><br>
194+
</div>
195+
<br>
196+
<div id="links"></div>
197+
198+
</body>
199+
</html>
200+

0 commit comments

Comments
 (0)