Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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...
commit 7e1a77063e3f002c118149c91d2e5e86b3bb2c5a 1 parent 872161c
@cpinkham cpinkham authored
Showing with 3,649 additions and 67 deletions.
  1. +1 −1  mythtv/bindings/perl/MythTV.pm
  2. +1 −1  mythtv/bindings/python/MythTV/static.py
  3. +16 −0 mythtv/html/js/fileutil.js
  4. +200 −0 mythtv/html/samples/livestream_rec.qsp
  5. +221 −0 mythtv/html/samples/livestream_sg.qsp
  6. +2 −2 mythtv/libs/libmythbase/mythversion.h
  7. +136 −0 mythtv/libs/libmythservicecontracts/datacontracts/liveStreamInfo.h
  8. +75 −0 mythtv/libs/libmythservicecontracts/datacontracts/liveStreamInfoList.h
  9. +2 −0  mythtv/libs/libmythservicecontracts/libmythservicecontracts.pro
  10. +26 −0 mythtv/libs/libmythservicecontracts/services/contentServices.h
  11. +679 −0 mythtv/libs/libmythtv/avformatwriter.cpp
  12. +63 −0 mythtv/libs/libmythtv/avformatwriter.h
  13. +37 −0 mythtv/libs/libmythtv/dbcheck.cpp
  14. +58 −0 mythtv/libs/libmythtv/filewriterbase.cpp
  15. +73 −0 mythtv/libs/libmythtv/filewriterbase.h
  16. +937 −0 mythtv/libs/libmythtv/httplivestream.cpp
  17. +117 −0 mythtv/libs/libmythtv/httplivestream.h
  18. +9 −0 mythtv/libs/libmythtv/libmythtv.pro
  19. +3 −1 mythtv/libs/libmythupnp/httprequest.cpp
  20. +252 −0 mythtv/programs/mythbackend/services/content.cpp
  21. +25 −0 mythtv/programs/mythbackend/services/content.h
  22. +29 −0 mythtv/programs/mythtranscode/commandlineparser.cpp
  23. +52 −8 mythtv/programs/mythtranscode/main.cpp
  24. +611 −54 mythtv/programs/mythtranscode/transcode.cpp
  25. +24 −0 mythtv/programs/mythtranscode/transcode.h
View
2  mythtv/bindings/perl/MythTV.pm
@@ -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
View
2  mythtv/bindings/python/MythTV/static.py
@@ -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'
View
16 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( /\.[^\.]$/, '' );
+}
+
View
200 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>
+
View
221 mythtv/html/samples/livestream_sg.qsp
@@ -0,0 +1,221 @@
+<html>
+<head>
+<title><i18n>HTTP Live Stream Demo</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 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) {
+ $("#LiveStreams").html("<i18n>Waiting</i18n>...");
+ $.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) {
+ $("#LiveStreams").html("<i18n>Waiting</i18n>...");
+ $.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();
+
+ $("#LiveStreams").html("<i18n>Waiting</i18n>...");
+ $.getJSON("/Content/AddLiveStream", datastr, function(data) {
+ listLiveStreams();
+ });
+}
+
+function listFiles() {
+ var group = $("#sgName").val();
+ var filter = $("#sgFilter").val().toLowerCase();
+ $("#links").html("<i18n>Loading</i18n>...");
+ $.getJSON("/Content/GetFileList", { StorageGroup: 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.QStringList, function(i, value) {
+ if ((filter.length == 0) ||
+ (value.toLowerCase().indexOf(filter) >= 0)) {
+ var tmpGroup = "'" + group + "'";
+ var tmpValue = "'" + value + "'";
+ str +=
+ '<input type=radio Name="FileName" Value="' + value + '">' +
+ value + "<br>";
+ }
+ });
+ str += "</form>";
+ $("#links").html(str);
+ });
+}
+</script>
+
+<font size=+1><i18n>HTTP Live Streams Demo</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>Storage Group</i18n>:
+ <select id="sgName">
+<%
+ var myth = new Myth();
+
+ var hostName = myth.GetHostName();
+ var list = myth.GetStorageGroupDirs("", "");
+ var seenHosts = new Array();
+
+ for (var nIdx=0; nIdx < list.StorageGroupDirs.length; nIdx++)
+ {
+ var sgDir = list.StorageGroupDirs[nIdx];
+ if (!seenHosts[sgDir.GroupName])
+ {
+ seenHosts[sgDir.GroupName] = 1;
+%>
+ <option value="<%= sgDir.GroupName %>"><%=sgDir.GroupName%></option>
+<%
+ }
+ }
+%>
+ </select><br>
+
+ <i18n>Filter</i18n>: <input id='sgFilter' size=20><br>
+ <input type='button' onClick='javascript:listFiles()' value='<i18n>List Files</i18n>'><br>
+</div>
+<br>
+<div id="links"></div>
+
+</body>
+</html>
+
View
4 mythtv/libs/libmythbase/mythversion.h
@@ -12,7 +12,7 @@
/// Update this whenever the plug-in API changes.
/// Including changes in the libmythbase, libmyth, libmythtv, libmythav* and
/// libmythui class methods used by plug-ins.
-#define MYTH_BINARY_VERSION "0.25.20111129-1"
+#define MYTH_BINARY_VERSION "0.25.20111201-1"
/** \brief Increment this whenever the MythTV network protocol changes.
*
@@ -51,7 +51,7 @@
* MythTV Python Bindings
* mythtv/bindings/python/MythTV/static.py
*/
-#define MYTH_DATABASE_VERSION "1287"
+#define MYTH_DATABASE_VERSION "1288"
MBASE_PUBLIC const char *GetMythSourceVersion();
View
136 mythtv/libs/libmythservicecontracts/datacontracts/liveStreamInfo.h
@@ -0,0 +1,136 @@
+#ifndef LIVESTREAMINFO_H_
+#define LIVESTREAMINFO_H_
+
+#include <QDateTime>
+#include <QString>
+
+#include "serviceexp.h"
+#include "datacontracthelper.h"
+
+namespace DTC
+{
+
+/////////////////////////////////////////////////////////////////////////////
+
+class SERVICE_PUBLIC LiveStreamInfo : public QObject
+{
+ Q_OBJECT
+ Q_CLASSINFO( "version" , "1.0" );
+
+ Q_PROPERTY( int Id READ Id WRITE setId )
+ Q_PROPERTY( int Width READ Width WRITE setWidth )
+ Q_PROPERTY( int Height READ Height WRITE setHeight )
+ Q_PROPERTY( int Bitrate READ Bitrate WRITE setBitrate )
+ Q_PROPERTY( int AudioBitrate READ AudioBitrate WRITE setAudioBitrate )
+ Q_PROPERTY( int SegmentSize READ SegmentSize WRITE setSegmentSize )
+ Q_PROPERTY( int MaxSegments READ MaxSegments WRITE setMaxSegments )
+ Q_PROPERTY( int StartSegment READ StartSegment WRITE setStartSegment )
+ Q_PROPERTY( int CurrentSegment READ CurrentSegment WRITE setCurrentSegment )
+ Q_PROPERTY( int SegmentCount READ SegmentCount WRITE setSegmentCount )
+ Q_PROPERTY( int PercentComplete READ PercentComplete WRITE setPercentComplete )
+ Q_PROPERTY( QDateTime Created READ Created WRITE setCreated )
+ Q_PROPERTY( QDateTime LastModified READ LastModified WRITE setLastModified )
+ Q_PROPERTY( QString RelativeURL READ RelativeURL WRITE setRelativeURL )
+ Q_PROPERTY( QString FullURL READ FullURL WRITE setFullURL )
+ Q_PROPERTY( QString StatusStr READ StatusStr WRITE setStatusStr )
+ Q_PROPERTY( int StatusInt READ StatusInt WRITE setStatusInt )
+ Q_PROPERTY( QString StatusMessage READ StatusMessage WRITE setStatusMessage )
+ Q_PROPERTY( QString SourceFile READ SourceFile WRITE setSourceFile )
+ Q_PROPERTY( QString SourceHost READ SourceHost WRITE setSourceHost )
+ Q_PROPERTY( int SourceWidth READ SourceWidth WRITE setSourceWidth )
+ Q_PROPERTY( int SourceHeight READ SourceHeight WRITE setSourceHeight )
+ Q_PROPERTY( int AudioOnlyBitrate READ AudioOnlyBitrate WRITE setAudioOnlyBitrate )
+
+ PROPERTYIMP ( int , Id )
+ PROPERTYIMP ( int , Width )
+ PROPERTYIMP ( int , Height )
+ PROPERTYIMP ( int , Bitrate )
+ PROPERTYIMP ( int , AudioBitrate )
+ PROPERTYIMP ( int , SegmentSize )
+ PROPERTYIMP ( int , MaxSegments )
+ PROPERTYIMP ( int , StartSegment )
+ PROPERTYIMP ( int , CurrentSegment )
+ PROPERTYIMP ( int , SegmentCount )
+ PROPERTYIMP ( int , PercentComplete )
+ PROPERTYIMP ( QDateTime , Created )
+ PROPERTYIMP ( QDateTime , LastModified )
+ PROPERTYIMP ( QString , RelativeURL )
+ PROPERTYIMP ( QString , FullURL )
+ PROPERTYIMP ( QString , StatusStr )
+ PROPERTYIMP ( int , StatusInt )
+ PROPERTYIMP ( QString , StatusMessage )
+ PROPERTYIMP ( QString , SourceFile )
+ PROPERTYIMP ( QString , SourceHost )
+ PROPERTYIMP ( int , SourceWidth )
+ PROPERTYIMP ( int , SourceHeight )
+ PROPERTYIMP ( int , AudioOnlyBitrate )
+
+ public:
+
+ static void InitializeCustomTypes()
+ {
+ qRegisterMetaType< LiveStreamInfo >();
+ qRegisterMetaType< LiveStreamInfo* >();
+ }
+
+ public:
+
+ LiveStreamInfo(QObject *parent = 0)
+ : QObject ( parent ),
+ m_Id ( 0 ),
+ m_Width ( 0 ),
+ m_Height ( 0 ),
+ m_Bitrate ( 0 ),
+ m_AudioBitrate ( 0 ),
+ m_SegmentSize ( 0 ),
+ m_MaxSegments ( 0 ),
+ m_StartSegment ( 0 ),
+ m_CurrentSegment ( 0 ),
+ m_SegmentCount ( 0 ),
+ m_PercentComplete ( 0 ),
+ m_StatusInt ( 0 ),
+ m_SourceWidth ( 0 ),
+ m_SourceHeight ( 0 ),
+ m_AudioOnlyBitrate ( 0 )
+ {
+ }
+
+ LiveStreamInfo( const LiveStreamInfo &src )
+ {
+ Copy( src );
+ }
+
+ void Copy( const LiveStreamInfo &src )
+ {
+ m_Id = src.m_Id ;
+ m_Width = src.m_Width ;
+ m_Height = src.m_Height ;
+ m_Bitrate = src.m_Bitrate ;
+ m_AudioBitrate = src.m_AudioBitrate ;
+ m_SegmentSize = src.m_SegmentSize ;
+ m_MaxSegments = src.m_MaxSegments ;
+ m_StartSegment = src.m_StartSegment ;
+ m_CurrentSegment = src.m_CurrentSegment ;
+ m_SegmentCount = src.m_SegmentCount ;
+ m_PercentComplete = src.m_PercentComplete ;
+ m_Created = src.m_Created ;
+ m_LastModified = src.m_LastModified ;
+ m_RelativeURL = src.m_RelativeURL ;
+ m_FullURL = src.m_FullURL ;
+ m_StatusStr = src.m_StatusStr ;
+ m_StatusInt = src.m_StatusInt ;
+ m_StatusMessage = src.m_StatusMessage ;
+ m_SourceFile = src.m_SourceFile ;
+ m_SourceHost = src.m_SourceHost ;
+ m_SourceWidth = src.m_SourceWidth ;
+ m_SourceHeight = src.m_SourceHeight ;
+ m_AudioOnlyBitrate = src.m_AudioOnlyBitrate ;
+ }
+};
+
+} // namespace DTC
+
+Q_DECLARE_METATYPE( DTC::LiveStreamInfo )
+Q_DECLARE_METATYPE( DTC::LiveStreamInfo* )
+
+#endif
View
75 mythtv/libs/libmythservicecontracts/datacontracts/liveStreamInfoList.h
@@ -0,0 +1,75 @@
+#ifndef LIVESTREAMINFOLIST_H_
+#define LIVESTREAMINFOLIST_H_
+
+#include <QVariantList>
+
+#include "serviceexp.h"
+#include "datacontracthelper.h"
+
+#include "liveStreamInfo.h"
+
+namespace DTC
+{
+
+class SERVICE_PUBLIC LiveStreamInfoList : public QObject
+{
+ Q_OBJECT
+ Q_CLASSINFO( "version", "1.0" );
+
+ // We need to know the type that will ultimately be contained in
+ // any QVariantList or QVariantMap. We do his by specifying
+ // A Q_CLASSINFO entry with "<PropName>_type" as the key
+ // and the type name as the value
+
+ Q_CLASSINFO( "LiveStreamInfos_type", "DTC::LiveStreamInfo");
+
+ Q_PROPERTY( QVariantList LiveStreamInfos READ LiveStreamInfos DESIGNABLE true )
+
+ PROPERTYIMP_RO_REF( QVariantList, LiveStreamInfos )
+
+ public:
+
+ static void InitializeCustomTypes()
+ {
+ qRegisterMetaType< LiveStreamInfoList >();
+ qRegisterMetaType< LiveStreamInfoList* >();
+
+ LiveStreamInfo::InitializeCustomTypes();
+ }
+
+ public:
+
+ LiveStreamInfoList(QObject *parent = 0)
+ : QObject( parent )
+ {
+ }
+
+ LiveStreamInfoList( const LiveStreamInfoList &src )
+ {
+ Copy( src );
+ }
+
+ void Copy( const LiveStreamInfoList &src )
+ {
+ CopyListContents< LiveStreamInfo >( this, m_LiveStreamInfos, src.m_LiveStreamInfos );
+ }
+
+ LiveStreamInfo *AddNewLiveStreamInfo()
+ {
+ // We must make sure the object added to the QVariantList has
+ // a parent of 'this'
+
+ LiveStreamInfo *pObject = new LiveStreamInfo( this );
+ m_LiveStreamInfos.append( QVariant::fromValue<QObject *>( pObject ));
+
+ return pObject;
+ }
+
+};
+
+} // namespace DTC
+
+Q_DECLARE_METATYPE( DTC::LiveStreamInfoList )
+Q_DECLARE_METATYPE( DTC::LiveStreamInfoList* )
+
+#endif
View
2  mythtv/libs/libmythservicecontracts/libmythservicecontracts.pro
@@ -37,6 +37,7 @@ HEADERS += datacontracts/captureCardList.h datacontracts/recRule.h
HEADERS += datacontracts/recRuleList.h datacontracts/artworkInfo.h
HEADERS += datacontracts/artworkInfoList.h datacontracts/frontendStatus.h
HEADERS += datacontracts/frontendActionList.h
+HEADERS += datacontracts/liveStreamInfo.h datacontracts/liveStreamInfoList.h
SOURCES += service.cpp
@@ -73,6 +74,7 @@ incDatacontracts.files += datacontracts/captureCard.h datacontracts/capt
incDatacontracts.files += datacontracts/recRule.h datacontracts/recRuleList.h
incDatacontracts.files += datacontracts/artworkInfo.h datacontracts/artworkInfoList.h
incDatacontracts.files += datacontracts/frontendStatus.h datacontracts/frontendActionList.h
+incDatacontracts.files += datacontracts/liveStreamInfo.h datacontracts/liveStreamInfoList.h
INSTALLS += inc incServices incDatacontracts
View
26 mythtv/libs/libmythservicecontracts/services/contentServices.h
@@ -29,6 +29,7 @@
#include "service.h"
#include "datacontracts/artworkInfoList.h"
+#include "datacontracts/liveStreamInfoList.h"
/////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////
@@ -60,6 +61,7 @@ class SERVICE_PUBLIC ContentServices : public Service //, public QScriptable ??
ContentServices( QObject *parent = 0 ) : Service( parent )
{
DTC::ArtworkInfoList::InitializeCustomTypes();
+ DTC::LiveStreamInfoList::InitializeCustomTypes();
}
public slots:
@@ -111,6 +113,30 @@ class SERVICE_PUBLIC ContentServices : public Service //, public QScriptable ??
virtual bool DownloadFile ( const QString &URL,
const QString &StorageGroup ) = 0;
+ virtual DTC::LiveStreamInfo *AddLiveStream ( const QString &StorageGroup,
+ const QString &FileName,
+ const QString &HostName,
+ const QString &MaxSegments,
+ const QString &Width,
+ const QString &Height,
+ const QString &Bitrate,
+ const QString &AudioBitrate,
+ const QString &SampleRate ) = 0;
+
+ virtual DTC::LiveStreamInfo *AddRecordingLiveStream ( int ChanId,
+ const QDateTime &StartTime,
+ const QString &MaxSegments,
+ const QString &Width,
+ const QString &Height,
+ const QString &Bitrate,
+ const QString &AudioBitrate,
+ const QString &SampleRate ) = 0;
+
+ virtual DTC::LiveStreamInfo *GetLiveStream ( int Id ) = 0;
+ virtual DTC::LiveStreamInfoList *GetLiveStreamList ( void ) = 0;
+
+ virtual DTC::LiveStreamInfo *StopLiveStream ( int Id ) = 0;
+ virtual bool RemoveLiveStream ( int Id ) = 0;
};
#endif
View
679 mythtv/libs/libmythtv/avformatwriter.cpp
@@ -0,0 +1,679 @@
+/* -*- Mode: c++ -*-
+ *
+ * Class AVFormatWriter
+ *
+ * Copyright (C) Chris Pinkham 2011
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include "mythlogging.h"
+#include "mythcorecontext.h"
+#include "NuppelVideoRecorder.h"
+#include "avformatwriter.h"
+
+#ifdef __linux__
+# include <byteswap.h> /* bswap_16|32|64 */
+#elif defined __APPLE__
+# include <libkern/OSByteOrder.h>
+# define bswap_16(x) OSSwapInt16(x)
+# define bswap_32(x) OSSwapInt32(x)
+# define bswap_64(x) OSSwapInt64(x)
+#else
+# error Byte swapping functions not defined for this platform
+#endif
+
+#define LOC QString("AVFW(%1): ").arg(m_filename)
+#define LOC_ERR QString("AVFW(%1) Error: ").arg(m_filename)
+#define LOC_WARN QString("AVFW(%1) Warning: ").arg(m_filename)
+
+AVFormatWriter::AVFormatWriter()
+ : FileWriterBase(),
+
+ m_avfRingBuffer(NULL), m_ringBuffer(NULL),
+
+ m_ctx(NULL),
+ m_videoStream(NULL), m_avVideoCodec(NULL),
+ m_audioStream(NULL), m_avAudioCodec(NULL),
+ m_picture(NULL), m_tmpPicture(NULL),
+ m_videoOutBuf(NULL), m_videoOutBufSize(0),
+ m_audioSamples(NULL), m_audioOutBuf(NULL),
+ m_audioOutBufSize(0), m_audioInputFrameSize(0)
+{
+ av_register_all();
+ avcodec_register_all();
+
+ // bool debug = VERBOSE_LEVEL_CHECK(VB_LIBAV, LOG_ANY);
+ // av_log_set_level((debug) ? AV_LOG_DEBUG : AV_LOG_ERROR);
+ // av_log_set_callback(myth_av_log);
+}
+
+AVFormatWriter::~AVFormatWriter()
+{
+ QMutexLocker locker(avcodeclock);
+
+ if (m_pkt)
+ {
+ delete m_pkt;
+ m_pkt = NULL;
+ }
+
+ if (m_ctx)
+ {
+ av_write_trailer(m_ctx);
+ url_fclose(m_ctx->pb);
+ for(unsigned int i = 0; i < m_ctx->nb_streams; i++) {
+ av_freep(&m_ctx->streams[i]);
+ }
+
+ av_free(m_ctx);
+ m_ctx = NULL;
+ }
+
+ if (m_videoOutBuf)
+ delete [] m_videoOutBuf;
+}
+
+bool AVFormatWriter::Init(void)
+{
+ if (m_videoOutBuf)
+ delete [] m_videoOutBuf;
+
+ if (m_width && m_height)
+ m_videoOutBuf = new unsigned char[m_width * m_height * 2 + 10];
+
+ AVOutputFormat *fmt = av_guess_format(m_container.toAscii().constData(),
+ NULL, NULL);
+ if (!fmt)
+ {
+ LOG(VB_RECORD, LOG_ERR, LOC +
+ QString("Init(): Unable to guess AVOutputFormat from container %1")
+ .arg(m_container));
+ return false;
+ }
+
+ m_fmt = *fmt;
+
+ if (m_width && m_height)
+ {
+ m_avVideoCodec = avcodec_find_encoder_by_name(
+ m_videoCodec.toAscii().constData());
+ if (!m_avVideoCodec)
+ {
+ LOG(VB_RECORD, LOG_ERR, LOC +
+ QString("Init(): Unable to find video codec %1").arg(m_videoCodec));
+ return false;
+ }
+
+ m_fmt.video_codec = m_avVideoCodec->id;
+ }
+ else
+ m_fmt.video_codec = CODEC_ID_NONE;
+
+ m_avAudioCodec = avcodec_find_encoder_by_name(
+ m_audioCodec.toAscii().constData());
+ if (!m_avAudioCodec)
+ {
+ LOG(VB_RECORD, LOG_ERR, LOC +
+ QString("Init(): Unable to find audio codec %1").arg(m_audioCodec));
+ return false;
+ }
+
+ m_fmt.audio_codec = m_avAudioCodec->id;
+
+ m_ctx = avformat_alloc_context();
+ if (!m_ctx)
+ {
+ LOG(VB_RECORD, LOG_ERR,
+ LOC + "Init(): Unable to allocate AVFormatContext");
+ return false;
+ }
+
+ m_ctx->oformat = &m_fmt;
+
+ if (m_container == "mpegts")
+ m_ctx->packet_size = 2324;
+
+ snprintf(m_ctx->filename, sizeof(m_ctx->filename), "%s",
+ m_filename.toAscii().constData());
+
+ if (m_fmt.video_codec != CODEC_ID_NONE)
+ m_videoStream = AddVideoStream();
+ if (m_fmt.audio_codec != CODEC_ID_NONE)
+ m_audioStream = AddAudioStream();
+
+ m_pkt = new AVPacket;
+ if (!m_pkt)
+ {
+ LOG(VB_RECORD, LOG_ERR, LOC + "Init(): error allocating AVPacket");
+ return false;
+ }
+
+ if (av_set_parameters(m_ctx, NULL) < 0)
+ {
+ LOG(VB_RECORD, LOG_ERR, "Init(): Invalid output format parameters");
+ return false;
+ }
+
+ if ((m_videoStream) && (!OpenVideo()))
+ {
+ LOG(VB_RECORD, LOG_ERR, LOC + "Init(): OpenVideo() failed");
+ return false;
+ }
+
+ if ((m_audioStream) && (!OpenAudio()))
+ {
+ LOG(VB_RECORD, LOG_ERR, LOC + "Init(): OpenAudio() failed");
+ return false;
+ }
+
+ return true;
+}
+
+bool AVFormatWriter::OpenFile(void)
+{
+ if (!(m_fmt.flags & AVFMT_NOFILE))
+ {
+ if (url_fopen(&m_ctx->pb, m_filename.toAscii().constData(), URL_WRONLY)
+ < 0)
+ {
+ LOG(VB_RECORD, LOG_ERR, LOC + "OpenFile(): url_fopen() failed");
+ return false;
+ }
+ }
+
+ m_ringBuffer = RingBuffer::Create(m_filename, true);
+
+ if (!m_ringBuffer)
+ {
+ LOG(VB_RECORD, LOG_ERR, LOC +
+ "OpenFile(): RingBuffer::Create() failed");
+ return false;
+ }
+
+ m_avfRingBuffer = new AVFRingBuffer(m_ringBuffer);
+ URLContext *uc = (URLContext *)m_ctx->pb->opaque;
+ uc->prot = &AVF_RingBuffer_Protocol;
+ uc->priv_data = (void *)m_avfRingBuffer;
+
+ av_write_header(m_ctx);
+
+ return true;
+}
+
+bool AVFormatWriter::CloseFile(void)
+{
+ if (m_ctx)
+ {
+ av_write_trailer(m_ctx);
+ url_fclose(m_ctx->pb);
+ for(unsigned int i = 0; i < m_ctx->nb_streams; i++) {
+ av_freep(&m_ctx->streams[i]);
+ }
+
+ av_free(m_ctx);
+ m_ctx = NULL;
+ }
+
+ return true;
+}
+
+bool AVFormatWriter::NextFrameIsKeyFrame(void)
+{
+ if ((m_framesWritten % m_keyFrameDist) == 0)
+ return true;
+
+ return false;
+}
+
+bool AVFormatWriter::WriteVideoFrame(VideoFrame *frame)
+{
+ int ret;
+ AVCodecContext *c;
+
+ c = m_videoStream->codec;
+
+ int csize = 0;
+ uint8_t *planes[3];
+ int len = frame->size;
+ unsigned char *buf = frame->buf;
+
+ planes[0] = buf;
+ planes[1] = planes[0] + frame->width * frame->height;
+ planes[2] = planes[1] + (frame->width * frame->height) /
+ 4; // (pictureFormat == PIX_FMT_YUV422P ? 2 : 4);
+
+ m_picture->data[0] = planes[0];
+ m_picture->data[1] = planes[1];
+ m_picture->data[2] = planes[2];
+ m_picture->linesize[0] = frame->width;
+ m_picture->linesize[1] = frame->width / 2;
+ m_picture->linesize[2] = frame->width / 2;
+ m_picture->pts = m_framesWritten + 1;
+ m_picture->type = FF_BUFFER_TYPE_SHARED;
+
+ if ((m_framesWritten % m_keyFrameDist) == 0)
+ m_picture->pict_type = FF_I_TYPE;
+ else
+ m_picture->pict_type = 0;
+
+ {
+ QMutexLocker locker(avcodeclock);
+ csize = avcodec_encode_video(m_videoStream->codec,
+ (unsigned char *)m_videoOutBuf,
+ len, m_picture);
+ }
+
+ if (!csize)
+ {
+ // LOG(VB_RECORD, LOG_ERR, QString("WriteVideoFrame(): cs: %1, mfw: %2, tc: %3, fn: %4").arg(csize).arg(m_framesWritten).arg(frame->timecode).arg(frame->frameNumber));
+ return false;
+ }
+
+ av_init_packet(m_pkt);
+
+ if ((m_framesWritten % m_keyFrameDist) == 0)
+ m_pkt->flags |= PKT_FLAG_KEY;
+
+ long long tc = frame->timecode;
+ if (m_startingTimecodeOffset == -1)
+ m_startingTimecodeOffset = tc;
+ tc -= m_startingTimecodeOffset;
+
+ m_pkt->pts = tc * m_videoStream->time_base.den / m_videoStream->time_base.num / 1000;
+ m_pkt->dts = AV_NOPTS_VALUE;
+ m_pkt->data = (uint8_t*)m_videoOutBuf;
+ m_pkt->size = csize;
+ m_pkt->stream_index= m_videoStream->index;
+
+ // LOG(VB_RECORD, LOG_ERR, QString("WriteVideoFrame(): cs: %1, mfw: %2, pkt->pts: %3, tc: %4, fn: %5, pic->pts: %6").arg(csize).arg(m_framesWritten).arg(m_pkt->pts).arg(frame->timecode).arg(frame->frameNumber).arg(m_picture->pts));
+ ret = av_interleaved_write_frame(m_ctx, m_pkt);
+ if (ret != 0)
+ LOG(VB_RECORD, LOG_ERR, LOC + "WriteVideoFrame(): "
+ "av_interleaved_write_frame couldn't write Video");
+
+ m_framesWritten++;
+
+ return true;
+}
+
+bool AVFormatWriter::WriteAudioFrame(unsigned char *buf, int fnum, int timecode)
+{
+ int csize = 0;
+
+#ifdef WORDS_BIGENDIAN
+ int sample_cnt = m_audioBufferSize / m_audioBytesPerSample;
+ bswap_16_buf((short int*) buf, sample_cnt, audioChannels);
+#endif
+
+ {
+ QMutexLocker locker(avcodeclock);
+ csize = avcodec_encode_audio(m_audioStream->codec, m_audioOutBuf,
+ m_audioOutBufSize, (short int *)buf);
+ }
+
+ if (!csize)
+ {
+ // LOG(VB_RECORD, LOG_ERR, QString("WriteAudioFrame(): cs: %1, mfw: %2, tc: %3, fn: %4").arg(csize).arg(m_framesWritten).arg(timecode).arg(fnum));
+ return false;
+ }
+
+ av_init_packet(m_pkt);
+
+ long long tc = timecode;
+ if (m_startingTimecodeOffset == -1)
+ m_startingTimecodeOffset = tc;
+ tc -= m_startingTimecodeOffset;
+
+ if (m_avVideoCodec)
+ m_pkt->pts = tc * m_videoStream->time_base.den / m_videoStream->time_base.num / 1000;
+ else
+ m_pkt->pts = tc * m_audioStream->time_base.den / m_audioStream->time_base.num / 1000;
+
+ m_pkt->dts = AV_NOPTS_VALUE;
+ m_pkt->flags |= AV_PKT_FLAG_KEY;
+ m_pkt->data = (uint8_t*)m_audioOutBuf;
+ m_pkt->size = csize;
+ m_pkt->stream_index = m_audioStream->index;
+
+ // LOG(VB_RECORD, LOG_ERR, QString("WriteAudioFrame(): cs: %1, mfw: %2, pkt->pts: %3, tc: %4, fn: %5").arg(csize).arg(m_framesWritten).arg(m_pkt->pts).arg(timecode).arg(fnum));
+
+ int ret = av_interleaved_write_frame(m_ctx, m_pkt);
+ if (ret != 0)
+ LOG(VB_RECORD, LOG_ERR, LOC + "WriteAudioFrame(): "
+ "av_interleaved_write_frame couldn't write Audio");
+
+ return true;
+}
+
+bool AVFormatWriter::WriteTextFrame(int vbimode, unsigned char *buf, int len,
+ int timecode, int pagenr)
+{
+ return true;
+}
+
+bool AVFormatWriter::ReOpen(QString filename)
+{
+ bool result = m_ringBuffer->ReOpen(filename);
+
+ if (result)
+ m_filename = filename;
+
+ return result;
+}
+
+AVStream* AVFormatWriter::AddVideoStream(void)
+{
+ AVCodecContext *c;
+ AVStream *st;
+
+ st = av_new_stream(m_ctx, 0);
+ if (!st)
+ {
+ LOG(VB_RECORD, LOG_ERR,
+ LOC + "AddVideoStream(): av_new_stream() failed");
+ return NULL;
+ }
+
+ c = st->codec;
+
+ c->codec_id = m_ctx->oformat->video_codec;
+ c->codec_type = AVMEDIA_TYPE_VIDEO;
+ c->bit_rate = m_videoBitrate;
+ c->width = m_width;
+ c->height = m_height;
+
+ // c->sample_aspect_ratio.num = (int)floor(m_aspect * 10000);
+ // c->sample_aspect_ratio.den = 10000;
+
+ c->time_base = GetCodecTimeBase();
+
+ st->time_base.den = 90000;
+ st->time_base.num = 1;
+ st->r_frame_rate.num = 0;
+ st->r_frame_rate.den = 0;
+
+ c->gop_size = m_keyFrameDist;
+ c->pix_fmt = PIX_FMT_YUV420P;
+ c->thread_count = m_encodingThreadCount;
+
+ if (c->codec_id == CODEC_ID_MPEG2VIDEO) {
+ c->max_b_frames = 2;
+ }
+ else if (c->codec_id == CODEC_ID_MPEG1VIDEO)
+ {
+ c->mb_decision = 2;
+ }
+ else if (c->codec_id == CODEC_ID_H264)
+ {
+ c->coder_type = 0;
+ c->max_b_frames = 0;
+ c->slices = 8;
+ c->level = 13;
+ c->flags |= CODEC_FLAG_LOOP_FILTER;
+ c->me_cmp |= 1;
+ c->partitions |= X264_PART_I8X8
+ + X264_PART_I4X4
+ + X264_PART_P8X8
+ + X264_PART_B8X8;
+ c->me_method = ME_HEX;
+ c->me_subpel_quality = 6;
+ c->me_range = 16;
+ c->keyint_min = 25;
+ c->scenechange_threshold = 40;
+ c->i_quant_factor = 0.71;
+ c->b_frame_strategy = 1;
+ c->qcompress = 0.6;
+ c->qmin = 10;
+ c->qmax = 51;
+ c->max_qdiff = 4;
+ c->refs = 3;
+ c->directpred = 1;
+ c->rc_lookahead = 0;
+
+ c->flags2 |= CODEC_FLAG2_FASTPSKIP;
+ c->flags2 |= CODEC_FLAG2_8X8DCT;
+ c->flags2 ^= CODEC_FLAG2_8X8DCT;
+ c->flags2 |= CODEC_FLAG2_WPRED;
+ c->flags2 ^= CODEC_FLAG2_WPRED;
+ }
+
+ if(m_ctx->oformat->flags & AVFMT_GLOBALHEADER)
+ c->flags |= CODEC_FLAG_GLOBAL_HEADER;
+
+ // LOG(VB_RECORD, LOG_ERR, LOC + QString("AddVideoStream(): br: %1, gs: %2, tb: %3/%4, w: %5, h: %6").arg(c->bit_rate).arg(c->gop_size).arg(c->time_base.den).arg(c->time_base.num).arg(c->width).arg(c->height));
+
+ return st;
+}
+
+bool AVFormatWriter::OpenVideo(void)
+{
+ AVCodec *codec;
+ AVCodecContext *c;
+
+ c = m_videoStream->codec;
+
+ codec = avcodec_find_encoder(c->codec_id);
+ if (!codec)
+ {
+ LOG(VB_RECORD, LOG_ERR,
+ LOC + "OpenVideo(): avcodec_find_encoder() failed");
+ return false;
+ }
+
+ if (avcodec_open(c, codec) < 0)
+ {
+ LOG(VB_RECORD, LOG_ERR,
+ LOC + "OpenVideo(): avcodec_open() failed");
+ return false;
+ }
+
+ m_videoOutBuf = NULL;
+ if (!(m_ctx->oformat->flags & AVFMT_RAWPICTURE)) {
+ m_videoOutBufSize = 200000;
+ m_videoOutBuf = (unsigned char *)av_malloc(m_videoOutBufSize);
+ }
+
+ m_picture = AllocPicture(c->pix_fmt);
+ if (!m_picture)
+ {
+ LOG(VB_RECORD, LOG_ERR,
+ LOC + "OpenVideo(): AllocPicture() failed");
+ return false;
+ }
+
+ m_tmpPicture = NULL;
+ if (c->pix_fmt != PIX_FMT_YUV420P)
+ {
+ m_tmpPicture = AllocPicture(PIX_FMT_YUV420P);
+ if (!m_tmpPicture)
+ {
+ LOG(VB_RECORD, LOG_ERR,
+ LOC + "OpenVideo(): m_tmpPicture AllocPicture() failed");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+AVStream* AVFormatWriter::AddAudioStream(void)
+{
+ AVCodecContext *c;
+ AVStream *st;
+
+ st = av_new_stream(m_ctx, 1);
+ if (!st)
+ {
+ LOG(VB_RECORD, LOG_ERR,
+ LOC + "AddAudioStream(): av_new_stream() failed");
+ return NULL;
+ }
+
+ c = st->codec;
+ c->codec_id = m_ctx->oformat->audio_codec;
+ c->codec_type = AVMEDIA_TYPE_AUDIO;
+
+ c->sample_fmt = AV_SAMPLE_FMT_S16;
+
+ c->bit_rate = m_audioBitrate;
+ c->sample_rate = m_audioSampleRate;
+ c->channels = m_audioChannels;
+
+ // c->flags |= CODEC_FLAG_QSCALE; // VBR
+ // c->global_quality = blah;
+
+ if (!m_avVideoCodec)
+ {
+ // c->time_base = (AVRational){1, m_audioSampleRate};
+ // st->time_base = c->time_base;
+ c->time_base = GetCodecTimeBase();
+ st->time_base.den = 90000;
+ st->time_base.num = 1;
+ }
+
+ // LOG(VB_RECORD, LOG_ERR, LOC + QString("AddAudioStream(): br: %1, sr, %2, c: %3, tb: %4/%5").arg(c->bit_rate).arg(c->sample_rate).arg(c->channels).arg(c->time_base.den).arg(c->time_base.num));
+
+ if (c->codec_id == CODEC_ID_AAC)
+ c->profile = FF_PROFILE_AAC_MAIN;
+
+ if(m_ctx->oformat->flags & AVFMT_GLOBALHEADER)
+ c->flags |= CODEC_FLAG_GLOBAL_HEADER;
+
+ return st;
+}
+
+bool AVFormatWriter::OpenAudio(void)
+{
+ AVCodecContext *c;
+ AVCodec *codec;
+
+ c = m_audioStream->codec;
+
+ codec = avcodec_find_encoder(c->codec_id);
+ if (!codec)
+ {
+ LOG(VB_RECORD, LOG_ERR,
+ LOC + "OpenAudio(): avcodec_find_encoder() failed");
+ return false;
+ }
+
+ if (avcodec_open(c, codec) < 0)
+ {
+ LOG(VB_RECORD, LOG_ERR,
+ LOC + "OpenAudio(): avcodec_open() failed");
+ return false;
+ }
+
+ m_audioFrameSize = c->frame_size;
+
+ m_audioOutBufSize = (int)(1.25 * 16384 * 7200);
+ //m_audioOutBufSize = c->frame_size * 2 * c->channels;
+
+ m_audioOutBuf = (unsigned char *)av_malloc(m_audioOutBufSize);
+
+ if (c->frame_size <= 1) {
+ m_audioInputFrameSize = m_audioOutBufSize / c->channels;
+ switch(m_audioStream->codec->codec_id) {
+ case CODEC_ID_PCM_S16LE:
+ case CODEC_ID_PCM_S16BE:
+ case CODEC_ID_PCM_U16LE:
+ case CODEC_ID_PCM_U16BE:
+ m_audioInputFrameSize >>= 1;
+ break;
+ default:
+ break;
+ }
+ } else {
+ m_audioInputFrameSize = c->frame_size;
+ }
+ m_audioSamples =
+ (unsigned int *)av_malloc(m_audioInputFrameSize * 2 * c->channels);
+
+ return true;
+}
+
+AVFrame* AVFormatWriter::AllocPicture(enum PixelFormat pix_fmt)
+{
+ AVFrame *picture;
+ unsigned char *picture_buf;
+ int size;
+
+ picture = avcodec_alloc_frame();
+ if (!picture)
+ {
+ LOG(VB_RECORD, LOG_ERR,
+ LOC + "AllocPicture(): avcodec_alloc_frame() failed");
+ return NULL;
+ }
+ size = avpicture_get_size(pix_fmt, m_width, m_height);
+ picture_buf = (unsigned char *)av_malloc(size);
+ if (!picture_buf)
+ {
+ LOG(VB_RECORD, LOG_ERR, LOC + "AllocPicture(): av_malloc() failed");
+ av_free(picture);
+ return NULL;
+ }
+ avpicture_fill((AVPicture *)picture, picture_buf,
+ pix_fmt, m_width, m_height);
+ return picture;
+}
+
+AVRational AVFormatWriter::GetCodecTimeBase(void)
+{
+ AVRational result;
+
+ result.den = (int)floor(m_frameRate * 100);
+ result.num = 100;
+
+ if (m_avVideoCodec && m_avVideoCodec->supported_framerates) {
+ const AVRational *p= m_avVideoCodec->supported_framerates;
+ AVRational req =
+ (AVRational){result.den, result.num};
+ const AVRational *best = NULL;
+ AVRational best_error= (AVRational){INT_MAX, 1};
+ for(; p->den!=0; p++) {
+ AVRational error = av_sub_q(req, *p);
+ if (error.num <0)
+ error.num *= -1;
+ if (av_cmp_q(error, best_error) < 0) {
+ best_error = error;
+ best = p;
+ }
+ }
+
+ if (best && best->num && best->den)
+ {
+ result.den = best->num;
+ result.num = best->den;
+ }
+ }
+
+ if (result.den == 2997)
+ {
+ result.den = 30000;
+ result.num = 1001;
+ }
+ else if (result.den == 5994)
+ {
+ result.den = 60000;
+ result.num = 1001;
+ }
+
+ return result;
+}
+
+/* vim: set expandtab tabstop=4 shiftwidth=4: */
+
View
63 mythtv/libs/libmythtv/avformatwriter.h
@@ -0,0 +1,63 @@
+#ifndef AVFORMATWRITER_H_
+#define AVFORMATWRITER_H_
+
+#include "filewriterbase.h"
+#include "avfringbuffer.h"
+
+#undef HAVE_AV_CONFIG_H
+extern "C" {
+#include "libavcodec/avcodec.h"
+#include "libavformat/avformat.h"
+}
+
+class AVFormatWriter : public FileWriterBase
+{
+ public:
+ AVFormatWriter();
+ ~AVFormatWriter();
+
+ bool Init(void);
+ bool OpenFile(void);
+ bool CloseFile(void);
+
+ bool WriteVideoFrame(VideoFrame *frame);
+ bool WriteAudioFrame(unsigned char *buf, int fnum, int timecode);
+ bool WriteTextFrame(int vbimode, unsigned char *buf, int len,
+ int timecode, int pagenr);
+
+ bool NextFrameIsKeyFrame(void);
+ bool ReOpen(QString filename);
+
+ private:
+ AVStream *AddVideoStream(void);
+ bool OpenVideo(void);
+ AVStream *AddAudioStream(void);
+ bool OpenAudio(void);
+ AVFrame *AllocPicture(enum PixelFormat pix_fmt);
+
+ AVRational GetCodecTimeBase(void);
+
+ AVFRingBuffer *m_avfRingBuffer;
+ RingBuffer *m_ringBuffer;
+
+ AVOutputFormat m_fmt;
+ AVFormatContext *m_ctx;
+ AVStream *m_videoStream;
+ AVCodec *m_avVideoCodec;
+ AVStream *m_audioStream;
+ AVCodec *m_avAudioCodec;
+ AVFrame *m_picture;
+ AVFrame *m_tmpPicture;
+ AVPacket *m_pkt;
+ unsigned char *m_videoOutBuf;
+ int m_videoOutBufSize;
+ unsigned int *m_audioSamples;
+ unsigned char *m_audioOutBuf;
+ int m_audioOutBufSize;
+ int m_audioInputFrameSize;
+};
+
+#endif
+
+/* vim: set expandtab tabstop=4 shiftwidth=4: */
+
View
37 mythtv/libs/libmythtv/dbcheck.cpp
@@ -5985,6 +5985,43 @@ NULL
return false;
}
+ if (dbver == "1287")
+ {
+ const char *updates[] = {
+"CREATE TABLE IF NOT EXISTS livestream ( "
+" id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, "
+" width INT UNSIGNED NOT NULL, "
+" height INT UNSIGNED NOT NULL, "
+" bitrate INT UNSIGNED NOT NULL, "
+" audiobitrate INT UNSIGNED NOT NULL, "
+" samplerate INT UNSIGNED NOT NULL, "
+" audioonlybitrate INT UNSIGNED NOT NULL, "
+" segmentsize INT UNSIGNED NOT NULL DEFAULT 10, "
+" maxsegments INT UNSIGNED NOT NULL DEFAULT 0, "
+" startsegment INT UNSIGNED NOT NULL DEFAULT 0, "
+" currentsegment INT UNSIGNED NOT NULL DEFAULT 0, "
+" segmentcount INT UNSIGNED NOT NULL DEFAULT 0, "
+" percentcomplete INT UNSIGNED NOT NULL DEFAULT 0, "
+" created DATETIME NOT NULL, "
+" lastmodified DATETIME NOT NULL, "
+" relativeurl VARCHAR(512) NOT NULL, "
+" fullurl VARCHAR(1024) NOT NULL, "
+" status INT UNSIGNED NOT NULL DEFAULT 0, "
+" statusmessage VARCHAR(256) NOT NULL, "
+" sourcefile VARCHAR(512) NOT NULL, "
+" sourcehost VARCHAR(64) NOT NULL, "
+" sourcewidth INT UNSIGNED NOT NULL DEFAULT 0, "
+" sourceheight INT UNSIGNED NOT NULL DEFAULT 0, "
+" outdir VARCHAR(256) NOT NULL, "
+" outbase VARCHAR(128) NOT NULL "
+") ENGINE=MyISAM DEFAULT CHARSET=utf8; ",
+NULL
+};
+
+ if (!performActualUpdate(updates, "1288", dbver))
+ return false;
+ }
+
return true;
}
View
58 mythtv/libs/libmythtv/filewriterbase.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode: c++ -*-
+ *
+ * Class FileWriterBase
+ *
+ * Copyright (C) Chris Pinkham 2011
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include "mythlogging.h"
+#include "filewriterbase.h"
+
+#define LOC QString("FWB(%1): ").arg(m_filename)
+#define LOC_ERR QString("FWB(%1) Error: ").arg(m_filename)
+
+FileWriterBase::FileWriterBase()
+ : m_videoBitrate(800000), m_width(0), m_height(0),
+ m_aspect(1.333333), m_frameRate(29.97), m_keyFrameDist(15),
+ m_audioBitrate(0), m_audioChannels(2), m_audioBits(16),
+ m_audioSampleRate(44100), m_audioBytesPerSample(2), m_audioFrameSize(-1),
+ m_encodingThreadCount(1),
+ m_framesWritten(0),
+ m_startingTimecodeOffset(-1)
+{
+}
+
+FileWriterBase::~FileWriterBase()
+{
+}
+
+bool FileWriterBase::WriteVideoFrame(VideoFrame *frame)
+{
+ LOG(VB_RECORD, LOG_ERR, LOC + "WriteVideoFrame(): Shouldn't be here!");
+
+ return false;
+}
+
+bool FileWriterBase::WriteAudioFrame(unsigned char *buf, int fnum, int timecode)
+{
+ LOG(VB_RECORD, LOG_ERR, LOC + "WriteAudioFrame(): Shouldn't be here!");
+
+ return false;
+}
+
+/* vim: set expandtab tabstop=4 shiftwidth=4: */
+
View
73 mythtv/libs/libmythtv/filewriterbase.h
@@ -0,0 +1,73 @@
+#ifndef FILEWRITERBASE_H
+#define FILEWRITERBASE_H
+
+#include <QString>
+
+#include "frame.h"
+
+class FileWriterBase
+{
+ public:
+ FileWriterBase();
+ virtual ~FileWriterBase();
+
+ virtual bool Init(void) { return true; }
+ virtual bool OpenFile(void) { return true; }
+ virtual bool CloseFile(void) { return true; }
+
+ virtual bool WriteVideoFrame(VideoFrame *frame);
+ virtual bool WriteAudioFrame(unsigned char *buf, int fnum, int timecode);
+ virtual bool WriteTextFrame(int vbimode, unsigned char *buf, int len,
+ int timecode, int pagenr) { return true; }
+ virtual bool WriteSeekTable(void) { return true; }
+
+ virtual bool SwitchToNextFile(void) { return false; }
+
+ void SetFilename(QString fname) { m_filename = fname; }
+ void SetContainer(QString cont) { m_container = cont; }
+ void SetVideoCodec(QString codec) { m_videoCodec = codec; }
+ void SetVideoBitrate(int bitrate) { m_videoBitrate = bitrate; }
+ void SetWidth(int width) { m_width = width; }
+ void SetHeight(int height) { m_height = height; }
+ void SetAspect(float aspect) { m_aspect = aspect; }
+ void SetFramerate(double rate) { m_frameRate = rate; }
+ void SetKeyFrameDist(int dist) { m_keyFrameDist = dist; }
+ void SetAudioCodec(QString codec) { m_audioCodec = codec; }
+ void SetAudioBitrate(int bitrate) { m_audioBitrate = bitrate; }
+ void SetAudioChannels(int channels) { m_audioChannels = channels; }
+ void SetAudioBits(int bits) { m_audioBits = bits; }
+ void SetAudioSampleRate(int rate) { m_audioSampleRate = rate; }
+ void SetAudioSampleBytes(int bps) { m_audioBytesPerSample = bps; }
+ void SetThreadCount(int count) { m_encodingThreadCount = count; }
+ void SetTimecodeOffset(long long o) { m_startingTimecodeOffset = o; }
+
+ long long GetFramesWritten(void) { return m_framesWritten; }
+ long long GetTimecodeOffset(void) { return m_startingTimecodeOffset; }
+ int GetAudioFrameSize(void) { return m_audioFrameSize; }
+
+ protected:
+ QString m_filename;
+ QString m_container;
+ QString m_videoCodec;
+ int m_videoBitrate;
+ int m_width;
+ int m_height;
+ float m_aspect;
+ double m_frameRate;
+ int m_keyFrameDist;
+ QString m_audioCodec;
+ int m_audioBitrate;
+ int m_audioChannels;
+ int m_audioBits;
+ int m_audioSampleRate;
+ int m_audioBytesPerSample;
+ int m_audioFrameSize;
+ int m_encodingThreadCount;
+ long long m_framesWritten;
+ long long m_startingTimecodeOffset;
+};
+
+#endif
+
+/* vim: set expandtab tabstop=4 shiftwidth=4: */
+
View
937 mythtv/libs/libmythtv/httplivestream.cpp
@@ -0,0 +1,937 @@
+/* -*- Mode: c++ -*-
+ *
+ * Class HTTPLiveStream
+ *
+ * Copyright (C) Chris Pinkham 2011
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include <stdio.h>
+
+#include <QFile>
+#include <QFileInfo>
+#include <QIODevice>
+#include <QRunnable>
+
+#include "mythcorecontext.h"
+#include "mythdirs.h"
+#include "mythtimer.h"
+#include "mthreadpool.h"
+#include "mythsystem.h"
+#include "exitcodes.h"
+#include "mythlogging.h"
+#include "storagegroup.h"
+#include "httplivestream.h"
+
+#define LOC QString("HLS(%1): ").arg(m_sourceFile)
+#define LOC_ERR QString("HLS(%1) Error: ").arg(m_sourceFile)
+#define SLOC QString("HLS(): ")
+#define SLOC_ERR QString("HLS() Error: ")
+
+/** \class HTTPLiveStreamThread
+ * \brief QRunnable class for running mythtranscode for HTTP Live Streams
+ *
+ * The HTTPLiveStreamThread class runs a mythtranscode command in
+ * non-blocking mode.
+ */
+class HTTPLiveStreamThread : public QRunnable
+{
+ public:
+ /** \fn HTTPLiveStreamThread::HTTPLiveStreamThread(int)
+ * \brief Constructor for creating a SystemEventThread
+ * \param cmd Command line to run for this System Event
+ * \param eventName Optional System Event name for this command
+ */
+
+ HTTPLiveStreamThread(int streamid)
+ : m_streamID(streamid) {}
+
+ /** \fn HTTPLiveStreamThread::run()
+ * \brief Runs mythtranscode for the given HTTP Live Stream ID
+ *
+ * Overrides QRunnable::run()
+ */
+ void run(void)
+ {
+ uint flags = kMSDontBlockInputDevs;
+
+ QString command = GetInstallPrefix() +
+ QString("/bin/mythtranscode --hls --hlsstreamid %1")
+ .arg(m_streamID) + logPropagateArgs;
+
+ uint result = myth_system(command, flags);
+
+ if (result != GENERIC_EXIT_OK)
+ LOG(VB_GENERAL, LOG_WARNING, SLOC +
+ QString("Command '%1' returned %2")
+ .arg(command).arg(result));
+ }
+
+ private:
+ int m_streamID;
+};
+
+
+HTTPLiveStream::HTTPLiveStream(QString srcFile, uint16_t width, uint16_t height,
+ uint32_t bitrate, uint32_t abitrate,
+ uint16_t maxSegments, uint16_t segmentSize,
+ uint32_t aobitrate, uint16_t srate)
+ : m_writing(false),
+ m_streamid(-1), m_sourceFile(srcFile),
+ m_sourceWidth(0), m_sourceHeight(0),
+ m_segmentSize(segmentSize), m_maxSegments(maxSegments),
+ m_segmentCount(0), m_startSegment(0),
+ m_curSegment(0),
+ m_height(height), m_width(width),
+ m_bitrate(bitrate),
+ m_audioBitrate(abitrate), m_audioOnlyBitrate(aobitrate),
+ m_sampleRate(srate),
+ m_created(QDateTime::currentDateTime()),
+ m_lastModified(QDateTime::currentDateTime()),
+ m_percentComplete(0),
+ m_status(kHLSStatusUndefined)
+{
+ m_sourceHost = gCoreContext->GetHostName();
+
+ QFileInfo finfo(m_sourceFile);
+ m_outBase = finfo.fileName() +
+ QString(".%1x%2_%3kV_%4kA").arg(m_width).arg(m_height)
+ .arg(m_bitrate/1000).arg(m_audioBitrate/1000);
+ m_outFile = m_outBase + ".av";
+
+ if (m_audioOnlyBitrate)
+ m_audioOutFile = m_outBase +
+ QString(".ao_%1kA").arg(m_audioOnlyBitrate/1000);
+
+ m_httpPrefix = gCoreContext->GetSetting("HTTPLiveStreamPrefix", QString(
+ "http://%1:%2/Content/GetFile?StorageGroup=Streaming&FileName=")
+ .arg(gCoreContext->GetSetting("MasterServerIP"))
+ .arg(gCoreContext->GetSetting("BackendStatusPort")));
+
+ m_fullURL = m_httpPrefix + m_outBase + ".m3u8";
+
+ if (m_fullURL.contains("/Content/GetFile"))
+ m_relativeURL = "/Content/GetFile?StorageGroup=Streaming&FileName=" +
+ m_outBase + ".m3u8";
+ else
+ m_relativeURL = m_outBase + ".m3u8";
+
+ QStringList groupDirs =
+ StorageGroup::getGroupDirs("Streaming", gCoreContext->GetHostName());
+
+ QString defaultDir = GetConfDir() + "/tmp/hls";
+
+ if (!groupDirs.isEmpty())
+ defaultDir = groupDirs[0];
+
+ m_outDir = gCoreContext->GetSetting("HTTPLiveStreamDir", defaultDir);
+
+ AddStream();
+}
+
+HTTPLiveStream::HTTPLiveStream(int streamid)
+ : m_writing(false),
+ m_streamid(streamid)
+{
+ LoadFromDB();
+}
+
+HTTPLiveStream::~HTTPLiveStream()
+{
+ if (m_writing)
+ {
+ WritePlaylist(false, true);
+ if (m_audioOnlyBitrate)
+ WritePlaylist(true, true);
+ }
+}
+
+bool HTTPLiveStream::InitForWrite(void)
+{
+ m_writing = true;
+
+ WriteHTML();
+ WriteMetaPlaylist();
+
+ UpdateStatus(kHLSStatusStarting);
+ UpdateStatusMessage("Transcode Starting");
+
+ return true;
+}
+
+QString HTTPLiveStream::GetFilename(uint16_t segmentNumber, bool fileOnly,
+ bool audioOnly)
+{
+ QString filename = audioOnly ? m_audioOutFile : m_outFile;
+ filename += ".%1.ts";
+
+ if (!fileOnly)
+ filename = m_outDir + "/" + filename;
+
+ if (segmentNumber)
+ return filename.arg(segmentNumber, 6, 10, QChar('0'));
+ else
+ return filename.arg(1, 6, 10, QChar('0'));
+
+ return filename;
+}
+
+QString HTTPLiveStream::GetCurrentFilename(bool audioOnly)
+{
+ return GetFilename(m_curSegment, false, audioOnly);
+}
+
+int HTTPLiveStream::AddStream(void)
+{
+ m_status = kHLSStatusQueued;
+
+ MSqlQuery query(MSqlQuery::InitCon());
+ query.prepare(
+ "INSERT INTO livestream "
+ " ( width, height, bitrate, audiobitrate, segmentsize, "
+ " maxsegments, startsegment, currentsegment, segmentcount, "
+ " percentcomplete, created, lastmodified, relativeurl, "
+ " fullurl, status, statusmessage, sourcefile, sourcehost, "
+ " sourcewidth, sourceheight, outdir, outbase, "
+ " audioonlybitrate, samplerate ) "
+ "VALUES "
+ " ( :WIDTH, :HEIGHT, :BITRATE, :AUDIOBITRATE, :SEGMENTSIZE, "
+ " :MAXSEGMENTS, 0, 0, 0, "
+ " 0, :CREATED, :LASTMODIFIED, :RELATIVEURL, "
+ " :FULLURL, :STATUS, :STATUSMESSAGE, :SOURCEFILE, :SOURCEHOST, "
+ " :SOURCEWIDTH, :SOURCEHEIGHT, :OUTDIR, :OUTBASE, "
+ " :AUDIOONLYBITRATE, :SAMPLERATE ) ");
+ query.bindValue(":WIDTH", m_width);
+ query.bindValue(":HEIGHT", m_height);
+ query.bindValue(":BITRATE", m_bitrate);
+ query.bindValue(":AUDIOBITRATE", m_audioBitrate);
+ query.bindValue(":SEGMENTSIZE", m_segmentSize);
+ query.bindValue(":MAXSEGMENTS", m_maxSegments);
+ query.bindValue(":CREATED", m_created);
+ query.bindValue(":LASTMODIFIED", m_lastModified);
+ query.bindValue(":RELATIVEURL", m_relativeURL);
+ query.bindValue(":FULLURL", m_fullURL);
+ query.bindValue(":STATUS", (int)m_status);
+ query.bindValue(":STATUSMESSAGE",
+ QString("Waiting for mythtranscode startup."));
+ query.bindValue(":SOURCEFILE", m_sourceFile);
+ query.bindValue(":SOURCEHOST", gCoreContext->GetHostName());
+ query.bindValue(":SOURCEWIDTH", 0);
+ query.bindValue(":SOURCEHEIGHT", 0);
+ query.bindValue(":OUTDIR", m_outDir);
+ query.bindValue(":OUTBASE", m_outBase);
+ query.bindValue(":AUDIOONLYBITRATE", m_audioOnlyBitrate);
+ query.bindValue(":SAMPLERATE", m_sampleRate);
+
+ if (!query.exec())
+ {
+ LOG(VB_GENERAL, LOG_ERR, LOC + "LiveStream insert failed.");
+ return -1;
+ }
+
+ query.prepare(
+ "SELECT id "
+ "FROM livestream "
+ "WHERE outbase = :OUTBASE;");
+ query.bindValue(":OUTBASE", m_outBase);
+
+ if (!query.exec() || !query.next())
+ {
+ LOG(VB_GENERAL, LOG_ERR, LOC + "Unable to query LiveStream streamid.");
+ return -1;
+ }
+
+ m_streamid = query.value(0).toInt();
+
+ return m_streamid;
+}
+
+bool HTTPLiveStream::AddSegment(void)
+{
+ if (m_streamid == -1)
+ return false;
+
+ MSqlQuery query(MSqlQuery::InitCon());
+
+ ++m_curSegment;
+ ++m_segmentCount;
+
+ if (!m_startSegment)
+ m_startSegment = m_curSegment;
+
+ if ((m_maxSegments) &&
+ (m_segmentCount > (uint16_t)(m_maxSegments + 1)))
+ {
+ QString thisFile = GetFilename(m_startSegment);
+
+ if (!QFile::remove(thisFile))
+ LOG(VB_GENERAL, LOG_ERR, LOC +
+ QString("Unable to delete %1.").arg(thisFile));
+
+ ++m_startSegment;
+ --m_segmentCount;
+ }
+
+ SaveSegmentInfo();
+ WritePlaylist(false);
+
+ if (m_audioOnlyBitrate)
+ WritePlaylist(true);
+
+ return true;
+}
+
+QString HTTPLiveStream::GetHTMLPageName(void)
+{
+ QString outFile = m_outDir + "/" + m_outBase + ".html";
+ return outFile;
+}
+
+bool HTTPLiveStream::WriteHTML(void)
+{
+ QString outFile = m_outDir + "/" + m_outBase + ".html";
+ QFile file(outFile);
+
+ if (!file.open(QIODevice::WriteOnly))
+ {
+ LOG(VB_RECORD, LOG_ERR, QString("Error opening %1").arg(outFile));
+ return false;
+ }
+
+ file.write(
+ "<html>\n"
+ " <head>\n");
+ file.write(QString(
+ " <title>%1</title>\n").arg(m_sourceFile).toAscii().constData());
+ file.write(
+ " </head>\n"
+ " <body style='background-color:#FFFFFF;'>\n"
+ " <center>\n"
+ " <video controls>\n");
+
+ if (m_fullURL.contains("/Content/GetFile"))
+ file.write(QString(
+ " <source src='/Content/GetFile?StorageGroup=Streaming&FileName=%1.m3u8' />\n")
+ .arg(m_outBase).toAscii().constData());
+ else
+ file.write(QString(
+ " <source src='%1.m3u8' />\n")
+ .arg(m_outBase).toAscii().constData());
+
+ file.write(
+ " </video>\n"
+ " </center>\n"
+ " </body>\n"
+ "</html>\n");
+
+ file.close();
+
+ return true;
+}
+
+QString HTTPLiveStream::GetMetaPlaylistName(void)
+{
+ QString outFile = m_outDir + "/" + m_outBase + ".m3u8";
+ return outFile;
+}
+
+bool HTTPLiveStream::WriteMetaPlaylist(void)
+{
+ QString outFile = m_outDir + "/" + m_outBase + ".m3u8";
+ QFile file(outFile);
+
+ if (!file.open(QIODevice::WriteOnly))
+ {
+ LOG(VB_RECORD, LOG_ERR, QString("Error opening %1").arg(outFile));
+ return false;
+ }
+
+ file.write("#EXTM3U\n");
+ file.write(QString("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=%1\n")
+ .arg((int)((m_bitrate + m_audioBitrate) * 1.1)).toAscii());
+
+ if (m_fullURL.contains("/Content/GetFile"))
+ file.write(QString(
+ "/Content/GetFile?StorageGroup=Streaming&FileName=%1.m3u8\n")
+ .arg(m_outFile).toAscii());
+ else
+ file.write(QString("%1.m3u8\n").arg(m_outFile).toAscii());
+
+ if (m_audioOnlyBitrate)
+ {
+ file.write(QString("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=%1\n")
+ .arg((int)((m_audioOnlyBitrate) * 1.1)).toAscii());
+ if (m_fullURL.contains("/Content/GetFile"))
+ file.write(QString(
+ "/Content/GetFile?StorageGroup=Streaming&FileName=%1.m3u8\n")
+ .arg(m_audioOutFile).toAscii());
+ else
+ file.write(QString("%1.m3u8\n").arg(m_audioOutFile).toAscii());
+ }
+
+ file.close();
+
+ return true;
+}
+
+QString HTTPLiveStream::GetPlaylistName(bool audioOnly)
+{
+ if (audioOnly && m_audioOutFile.isEmpty())
+ return QString();
+
+ QString base = audioOnly ? m_audioOutFile : m_outFile;
+ QString outFile = m_outDir + "/" + base + ".m3u8";
+ return outFile;
+}
+
+bool HTTPLiveStream::WritePlaylist(bool audioOnly, bool writeEndTag)
+{
+ QString base = audioOnly ? m_audioOutFile : m_outFile;
+ QString outFile = m_outDir + "/" + base + ".m3u8";
+ QString tmpFile = m_outDir + "/" + base + ".m3u8.tmp";
+
+ QFile file(tmpFile);
+
+ if (!file.open(QIODevice::WriteOnly))
+ {
+ LOG(VB_RECORD, LOG_ERR, QString("Error opening %1").arg(tmpFile));
+ return false;
+ }
+
+ file.write("#EXTM3U\n");
+ file.write(QString("#EXT-X-TARGETDURATION:%1\n")
+ .arg(m_segmentSize).toAscii());
+ file.write(QString("#EXT-X-MEDIA-SEQUENCE:%1\n")
+ .arg(m_startSegment).toAscii());
+
+ if (writeEndTag)
+ file.write("#EXT-X-ENDLIST\n");
+
+ // Don't write out the current segment until the end
+ unsigned int tmpSegCount = m_segmentCount - 1;
+ unsigned int i = 0;
+ unsigned int segmentid = m_startSegment;
+
+ if (writeEndTag)
+ ++tmpSegCount;
+
+ while (i < tmpSegCount)
+ {
+ file.write(QString("#EXTINF:%1\n").arg(m_segmentSize).toAscii());
+ if (m_fullURL.contains("/Content/GetFile"))
+ file.write(QString(
+ "/Content/GetFile?StorageGroup=Streaming&FileName=%1\n")
+ .arg(GetFilename(segmentid + i, true, audioOnly)).toAscii());
+ else
+ file.write(QString("%1\n")
+ .arg(GetFilename(segmentid + i, true, audioOnly)).toAscii());
+
+ ++i;
+ }
+
+ file.close();
+
+ rename(tmpFile.toAscii().constData(), outFile.toAscii().constData());
+
+ return true;
+}
+
+bool HTTPLiveStream::SaveSegmentInfo(void)
+{
+ MSqlQuery query(MSqlQuery::InitCon());
+ query.prepare(
+ "UPDATE livestream "
+ "SET startsegment = :START, currentsegment = :CURRENT, "
+ " segmentcount = :COUNT "
+ "WHERE id = :STREAMID; ");
+ query.bindValue(":START", m_startSegment);
+ query.bindValue(":CURRENT", m_curSegment);
+ query.bindValue(":COUNT", m_segmentCount);
+ query.bindValue(":STREAMID", m_streamid);
+
+ if (query.exec())
+ return true;
+
+ LOG(VB_GENERAL, LOG_ERR, LOC +
+ QString("Unable to update segment info for streamid %1")
+ .arg(m_streamid));
+ return false;
+}
+
+bool HTTPLiveStream::UpdateSizeInfo(uint16_t width, uint16_t height,
+ uint16_t srcwidth, uint16_t srcheight)
+{
+ QFileInfo finfo(m_sourceFile);
+ QString newOutBase = finfo.fileName() +
+ QString(".%1x%2_%3kV_%4kA").arg(width).arg(height)
+ .arg(m_bitrate/1000).arg(m_audioBitrate/1000);
+ QString newFullURL = m_httpPrefix + newOutBase + ".m3u8";
+ QString newRelativeURL;
+
+ if (newFullURL.contains("/Content/GetFile"))
+ newRelativeURL = "/Content/GetFile?StorageGroup=Streaming&FileName=" +
+ newOutBase + ".m3u8";
+ else
+ newRelativeURL = newOutBase + ".m3u8";
+
+ MSqlQuery query(MSqlQuery::InitCon());
+ query.prepare(
+ "UPDATE livestream "
+ "SET width = :WIDTH, height = :HEIGHT, "
+ " sourcewidth = :SRCWIDTH, sourceheight = :SRCHEIGHT, "
+ " fullurl = :FULLURL, relativeurl = :RELATIVEURL, "
+ " outbase = :OUTBASE "
+ "WHERE id = :STREAMID; ");
+ query.bindValue(":WIDTH", width);
+ query.bindValue(":HEIGHT", height);
+ query.bindValue(":SRCWIDTH", srcwidth);
+ query.bindValue(":SRCHEIGHT", srcheight);
+ query.bindValue(":FULLURL", newFullURL);
+ query.bindValue(":RELATIVEURL", newRelativeURL);
+ query.bindValue(":OUTBASE", newOutBase);
+ query.bindValue(":STREAMID", m_streamid);
+
+ if (!query.exec())
+ {
+ LOG(VB_GENERAL, LOG_ERR, LOC +
+ QString("Unable to update segment info for streamid %1")
+ .arg(m_streamid));
+ return false;
+ }
+
+ m_width = width;
+ m_height = height;
+ m_sourceWidth = srcwidth;
+ m_sourceHeight = srcheight;
+ m_outBase = newOutBase;
+ m_fullURL = newFullURL;
+ m_relativeURL = newRelativeURL;
+
+ m_outFile = m_outBase + ".av";
+
+ if (m_audioOnlyBitrate)
+ m_audioOutFile = m_outBase +
+ QString(".ao_%1kA").arg(m_audioOnlyBitrate/1000);
+
+ m_httpPrefix = gCoreContext->GetSetting("HTTPLiveStreamPrefix", QString(
+ "http://%1:%2/Content/GetFile?StorageGroup=Streaming&FileName=")
+ .arg(gCoreContext->GetSetting("MasterServerIP"))
+ .arg(gCoreContext->GetSetting("BackendStatusPort")));
+
+ return true;
+}
+
+bool HTTPLiveStream::UpdateStatus(HTTPLiveStreamStatus status)
+{
+ if ((m_status == kHLSStatusStopping) &&
+ (status == kHLSStatusRunning))
+ {
+ LOG(VB_RECORD, LOG_DEBUG, LOC + "Attempted to switch from "
+ "Stopping to Running State");
+ return false;
+ }
+
+ QString statusStr = StatusToString(status);
+
+ m_status = status;
+
+ MSqlQuery query(MSqlQuery::InitCon());
+ query.prepare(
+ "UPDATE livestream "
+ "SET status = :STATUS "
+ "WHERE id = :STREAMID; ");
+ query.bindValue(":STATUS", (int)status);
+ query.bindValue(":STREAMID", m_streamid);
+
+ if (query.exec())
+ return true;
+
+ LOG(VB_GENERAL, LOG_ERR, LOC +
+ QString("Unable to update status for streamid %1").arg(m_streamid));
+ return false;
+}
+
+bool HTTPLiveStream::UpdateStatusMessage(QString message)
+{
+ MSqlQuery query(MSqlQuery::InitCon());
+ query.prepare(
+ "UPDATE livestream "
+ "SET statusmessage = :MESSAGE "
+ "WHERE id = :STREAMID; ");
+ query.bindValue(":MESSAGE", message);
+ query.bindValue(":STREAMID", m_streamid);
+
+ if (query.exec())
+ {
+ m_statusMessage = message;
+ return true;
+ }
+
+ LOG(VB_GENERAL, LOG_ERR, LOC +
+ QString("Unable to update status message for streamid %1")
+ .arg(m_streamid));
+ return false;
+}
+
+bool HTTPLiveStream::UpdatePercentComplete(int percent)
+{
+ MSqlQuery query(MSqlQuery::InitCon());
+ query.prepare(
+ "UPDATE livestream "
+ "SET percentcomplete = :PERCENT "
+ "WHERE id = :STREAMID; ");
+ query.bindValue(":PERCENT", percent);
+ query.bindValue(":STREAMID", m_streamid);
+
+ if (query.exec())
+ {
+ m_percentComplete = percent;
+ return true;
+ }
+
+ LOG(VB_GENERAL, LOG_ERR, LOC +
+ QString("Unable to update percent complete for streamid %1")
+ .arg(m_streamid));
+ return false;
+}
+
+QString HTTPLiveStream::StatusToString(HTTPLiveStreamStatus status)
+{
+ switch (m_status) {
+ case kHLSStatusUndefined : return QString("Undefined");
+ case kHLSStatusQueued : return QString("Queued");
+ case kHLSStatusStarting : return QString("Starting");
+ case kHLSStatusRunning : return QString("Running");
+ case kHLSStatusCompleted : return QString("Completed");
+ case kHLSStatusErrored : return QString("Errored");
+ case kHLSStatusStopping : return QString("Stopping");
+ case kHLSStatusStopped : return QString("Stopped");
+ };
+
+ return QString("Unknown status value");
+}
+
+bool HTTPLiveStream::LoadFromDB(void)
+{
+ MSqlQuery query(MSqlQuery::InitCon());
+ query.prepare(
+ "SELECT width, height, bitrate, audiobitrate, segmentsize, "
+ " maxsegments, startsegment, currentsegment, segmentcount, "
+ " percentcomplete, created, lastmodified, relativeurl, "
+ " fullurl, status, statusmessage, sourcefile, sourcehost, "
+ " sourcewidth, sourceheight, outdir, outbase, audioonlybitrate, "
+ " samplerate "
+ "FROM livestream "
+ "WHERE id = :STREAMID; ");
+ query.bindValue(":STREAMID", m_streamid);
+
+ if (!query.exec() || !query.next())
+ {
+ LOG(VB_GENERAL, LOG_ERR, LOC +
+ QString("Unable to query DB info for stream %1")
+ .arg(m_streamid));
+ return false;
+ }
+
+ m_width = query.value(0).toUInt();
+ m_height = query.value(1).toUInt();
+ m_bitrate = query.value(2).toUInt();
+ m_audioBitrate = query.value(3).toUInt();
+ m_segmentSize = query.value(4).toUInt();
+ m_maxSegments = query.value(5).toUInt();
+ m_startSegment = query.value(6).toUInt();
+ m_curSegment = query.value(7).toUInt();
+ m_segmentCount = query.value(8).toUInt();
+ m_percentComplete = query.value(9).toUInt();
+ m_created = query.value(10).toDateTime();
+ m_lastModified = query.value(11).toDateTime();
+ m_relativeURL = query.value(12).toString();
+ m_fullURL = query.value(13).toString();
+ m_status = (HTTPLiveStreamStatus)(query.value(14).toInt());
+ m_statusMessage = query.value(15).toString();
+ m_sourceFile = query.value(16).toString();
+ m_sourceHost = query.value(17).toString();
+ m_sourceWidth = query.value(18).toUInt();
+ m_sourceHeight = query.value(19).toUInt();
+ m_outDir = query.value(20).toString();
+ m_outBase = query.value(21).toString();
+ m_audioOnlyBitrate = query.value(22).toUInt();
+ m_sampleRate = query.value(23).toUInt();
+
+ m_httpPrefix = gCoreContext->GetSetting("HTTPLiveStreamPrefix", QString(
+ "http://%1:%2/Content/GetFile?StorageGroup=Streaming&FileName=")
+ .arg(gCoreContext->GetSetting("MasterServerIP"))
+ .arg(gCoreContext->GetSetting("BackendStatusPort")));
+
+ m_outFile = m_outBase + ".av";
+
+ if (m_audioOnlyBitrate)
+ m_audioOutFile = m_outBase +
+ QString(".ao_%1kA").arg(m_audioOnlyBitrate/1000);
+
+ return true;
+}
+
+HTTPLiveStreamStatus HTTPLiveStream::GetDBStatus(void)
+{
+ MSqlQuery query(MSqlQuery::InitCon());
+ query.prepare(
+ "SELECT status FROM livestream "
+ "WHERE id = :STREAMID; ");
+ query.bindValue(":STREAMID", m_streamid);
+
+ if (!query.exec() || !query.next())
+ {
+ LOG(VB_GENERAL, LOG_ERR, LOC +
+ QString("Unable to check stop status for stream %1")
+ .arg(m_streamid));
+ return kHLSStatusUndefined;
+ }
+
+ return (HTTPLiveStreamStatus)query.value(0).toInt();
+}
+
+bool HTTPLiveStream::CheckStop(void)
+{
+ MSqlQuery query(MSqlQuery::InitCon());
+ query.prepare(
+ "SELECT status FROM livestream "
+ "WHERE id = :STREAMID; ");
+ query.bindValue(":STREAMID", m_streamid);
+
+ if (!query.exec() || !query.next())
+ {
+ LOG(VB_GENERAL, LOG_ERR, LOC +
+ QString("Unable to check stop status for stream %1")
+ .arg(m_streamid));
+ return false;
+ }
+
+ if (query.value(0).toInt() == (int)kHLSStatusStopping)
+ return true;
+
+ return false;
+}
+
+DTC::LiveStreamInfo *HTTPLiveStream::StartStream(void)
+{
+ HTTPLiveStreamThread *streamThread =
+ new HTTPLiveStreamThread(GetStreamID());
+ MThreadPool::globalInstance()->startReserved(streamThread,
+ "HTTPLiveStream");
+ MythTimer statusTimer;
+ int delay = 250000;
+ statusTimer.start();
+
+ HTTPLiveStreamStatus status = GetDBStatus();
+ while ((status == kHLSStatusQueued) &&
+ ((statusTimer.elapsed() / 1000) < 30))
+ {
+ delay = (int)(delay * 1.5);
+ usleep(delay);
+
+ status = GetDBStatus();
+ }
+
+ return GetLiveStreamInfo();
+}
+
+bool HTTPLiveStream::RemoveStream(int id)
+{
+ MSqlQuery query(MSqlQuery::InitCon());
+ query.prepare(