From 7e1a77063e3f002c118149c91d2e5e86b3bb2c5a Mon Sep 17 00:00:00 2001 From: Chris Pinkham Date: Thu, 1 Dec 2011 01:08:48 -0500 Subject: [PATCH] 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. --- mythtv/bindings/perl/MythTV.pm | 2 +- mythtv/bindings/python/MythTV/static.py | 2 +- mythtv/html/js/fileutil.js | 16 + mythtv/html/samples/livestream_rec.qsp | 200 ++++ mythtv/html/samples/livestream_sg.qsp | 221 +++++ mythtv/libs/libmythbase/mythversion.h | 4 +- .../datacontracts/liveStreamInfo.h | 136 +++ .../datacontracts/liveStreamInfoList.h | 75 ++ .../libmythservicecontracts.pro | 2 + .../services/contentServices.h | 26 + mythtv/libs/libmythtv/avformatwriter.cpp | 679 +++++++++++++ mythtv/libs/libmythtv/avformatwriter.h | 63 ++ mythtv/libs/libmythtv/dbcheck.cpp | 37 + mythtv/libs/libmythtv/filewriterbase.cpp | 58 ++ mythtv/libs/libmythtv/filewriterbase.h | 73 ++ mythtv/libs/libmythtv/httplivestream.cpp | 937 ++++++++++++++++++ mythtv/libs/libmythtv/httplivestream.h | 117 +++ mythtv/libs/libmythtv/libmythtv.pro | 9 + mythtv/libs/libmythupnp/httprequest.cpp | 4 +- .../programs/mythbackend/services/content.cpp | 252 +++++ .../programs/mythbackend/services/content.h | 25 + .../mythtranscode/commandlineparser.cpp | 29 + mythtv/programs/mythtranscode/main.cpp | 60 +- mythtv/programs/mythtranscode/transcode.cpp | 665 ++++++++++++- mythtv/programs/mythtranscode/transcode.h | 24 + 25 files changed, 3649 insertions(+), 67 deletions(-) create mode 100644 mythtv/html/js/fileutil.js create mode 100644 mythtv/html/samples/livestream_rec.qsp create mode 100644 mythtv/html/samples/livestream_sg.qsp create mode 100644 mythtv/libs/libmythservicecontracts/datacontracts/liveStreamInfo.h create mode 100644 mythtv/libs/libmythservicecontracts/datacontracts/liveStreamInfoList.h create mode 100644 mythtv/libs/libmythtv/avformatwriter.cpp create mode 100644 mythtv/libs/libmythtv/avformatwriter.h create mode 100644 mythtv/libs/libmythtv/filewriterbase.cpp create mode 100644 mythtv/libs/libmythtv/filewriterbase.h create mode 100644 mythtv/libs/libmythtv/httplivestream.cpp create mode 100644 mythtv/libs/libmythtv/httplivestream.h diff --git a/mythtv/bindings/perl/MythTV.pm b/mythtv/bindings/perl/MythTV.pm index 91484ea2d8a..5e9720ff5ed 100644 --- a/mythtv/bindings/perl/MythTV.pm +++ b/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 diff --git a/mythtv/bindings/python/MythTV/static.py b/mythtv/bindings/python/MythTV/static.py index 994628a7f56..7c8b82f0b7d 100644 --- a/mythtv/bindings/python/MythTV/static.py +++ b/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' diff --git a/mythtv/html/js/fileutil.js b/mythtv/html/js/fileutil.js new file mode 100644 index 00000000000..b14edbfe2de --- /dev/null +++ b/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( /\.[^\.]$/, '' ); +} + diff --git a/mythtv/html/samples/livestream_rec.qsp b/mythtv/html/samples/livestream_rec.qsp new file mode 100644 index 00000000000..13bb7401bea --- /dev/null +++ b/mythtv/html/samples/livestream_rec.qsp @@ -0,0 +1,200 @@ + + +<i18n>HTTP Live Stream Demo2</i18n> + + + + + + +HTTP Live Streams Demo2 (Refresh)
+
+
+
+
+ + Recording Group: +
+ Filter:
+
+ +
+ + + + + diff --git a/mythtv/html/samples/livestream_sg.qsp b/mythtv/html/samples/livestream_sg.qsp new file mode 100644 index 00000000000..c4d20844c43 --- /dev/null +++ b/mythtv/html/samples/livestream_sg.qsp @@ -0,0 +1,221 @@ + + +<i18n>HTTP Live Stream Demo</i18n> + + + + + + +HTTP Live Streams Demo(Refresh)
+
+
+
+
+
+ Storage Group: +
+ + Filter:
+
+ +
+ + + + + diff --git a/mythtv/libs/libmythbase/mythversion.h b/mythtv/libs/libmythbase/mythversion.h index 9fb297a2a91..eb7524cdcba 100644 --- a/mythtv/libs/libmythbase/mythversion.h +++ b/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(); diff --git a/mythtv/libs/libmythservicecontracts/datacontracts/liveStreamInfo.h b/mythtv/libs/libmythservicecontracts/datacontracts/liveStreamInfo.h new file mode 100644 index 00000000000..c4538b20241 --- /dev/null +++ b/mythtv/libs/libmythservicecontracts/datacontracts/liveStreamInfo.h @@ -0,0 +1,136 @@ +#ifndef LIVESTREAMINFO_H_ +#define LIVESTREAMINFO_H_ + +#include +#include + +#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 diff --git a/mythtv/libs/libmythservicecontracts/datacontracts/liveStreamInfoList.h b/mythtv/libs/libmythservicecontracts/datacontracts/liveStreamInfoList.h new file mode 100644 index 00000000000..7a03b3eaa1f --- /dev/null +++ b/mythtv/libs/libmythservicecontracts/datacontracts/liveStreamInfoList.h @@ -0,0 +1,75 @@ +#ifndef LIVESTREAMINFOLIST_H_ +#define LIVESTREAMINFOLIST_H_ + +#include + +#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 "_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( pObject )); + + return pObject; + } + +}; + +} // namespace DTC + +Q_DECLARE_METATYPE( DTC::LiveStreamInfoList ) +Q_DECLARE_METATYPE( DTC::LiveStreamInfoList* ) + +#endif diff --git a/mythtv/libs/libmythservicecontracts/libmythservicecontracts.pro b/mythtv/libs/libmythservicecontracts/libmythservicecontracts.pro index 79f7f75aff6..1f5fa2cc49a 100644 --- a/mythtv/libs/libmythservicecontracts/libmythservicecontracts.pro +++ b/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 diff --git a/mythtv/libs/libmythservicecontracts/services/contentServices.h b/mythtv/libs/libmythservicecontracts/services/contentServices.h index e64fdbca6c3..6514a056001 100644 --- a/mythtv/libs/libmythservicecontracts/services/contentServices.h +++ b/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 diff --git a/mythtv/libs/libmythtv/avformatwriter.cpp b/mythtv/libs/libmythtv/avformatwriter.cpp new file mode 100644 index 00000000000..eecdfe634bc --- /dev/null +++ b/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 /* bswap_16|32|64 */ +#elif defined __APPLE__ +# include +# 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: */ + diff --git a/mythtv/libs/libmythtv/avformatwriter.h b/mythtv/libs/libmythtv/avformatwriter.h new file mode 100644 index 00000000000..bb10a39eb11 --- /dev/null +++ b/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: */ + diff --git a/mythtv/libs/libmythtv/dbcheck.cpp b/mythtv/libs/libmythtv/dbcheck.cpp index cde79d44208..a889641c1b7 100644 --- a/mythtv/libs/libmythtv/dbcheck.cpp +++ b/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; } diff --git a/mythtv/libs/libmythtv/filewriterbase.cpp b/mythtv/libs/libmythtv/filewriterbase.cpp new file mode 100644 index 00000000000..95150f012c3 --- /dev/null +++ b/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: */ + diff --git a/mythtv/libs/libmythtv/filewriterbase.h b/mythtv/libs/libmythtv/filewriterbase.h new file mode 100644 index 00000000000..a8d256fc95a --- /dev/null +++ b/mythtv/libs/libmythtv/filewriterbase.h @@ -0,0 +1,73 @@ +#ifndef FILEWRITERBASE_H +#define FILEWRITERBASE_H + +#include + +#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: */ + diff --git a/mythtv/libs/libmythtv/httplivestream.cpp b/mythtv/libs/libmythtv/httplivestream.cpp new file mode 100644 index 00000000000..10a2394758e --- /dev/null +++ b/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 + +#include +#include +#include +#include + +#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( + "\n" + " \n"); + file.write(QString( + " %1\n").arg(m_sourceFile).toAscii().constData()); + file.write( + " \n" + " \n" + "
\n" + " \n" + "
\n" + " \n" + "\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( + "SELECT startSegment, segmentCount " + "FROM livestream " + "WHERE id = :STREAMID; "); + query.bindValue(":STREAMID", id); + + if (!query.exec() || !query.next()) + { + LOG(VB_RECORD, LOG_ERR, "Error selecting stream info in RemoveStream"); + return false; + } + + QString outDir = gCoreContext->GetSetting("HTTPLiveStreamDir", "/tmp"); + HTTPLiveStream *hls = new HTTPLiveStream(id); + + if (hls->GetDBStatus() == kHLSStatusRunning) { + HTTPLiveStream::StopStream(id); + } + + QString thisFile; + int startSegment = query.value(0).toInt(); + int segmentCount = query.value(1).toInt(); + + for (int x = 0; x < segmentCount; ++x) + { + thisFile = hls->GetFilename(startSegment + x); + + if (!thisFile.isEmpty() && !QFile::remove(thisFile)) + LOG(VB_GENERAL, LOG_ERR, SLOC + + QString("Unable to delete %1.").arg(thisFile)); + + thisFile = hls->GetFilename(startSegment + x, false, true); + + if (!thisFile.isEmpty() && !QFile::remove(thisFile)) + LOG(VB_GENERAL, LOG_ERR, SLOC + + QString("Unable to delete %1.").arg(thisFile)); + } + + thisFile = hls->GetMetaPlaylistName(); + if (!thisFile.isEmpty() && !QFile::remove(thisFile)) + LOG(VB_GENERAL, LOG_ERR, SLOC + + QString("Unable to delete %1.").arg(thisFile)); + + thisFile = hls->GetPlaylistName(); + if (!thisFile.isEmpty() && !QFile::remove(thisFile)) + LOG(VB_GENERAL, LOG_ERR, SLOC + + QString("Unable to delete %1.").arg(thisFile)); + + thisFile = hls->GetPlaylistName(true); + if (!thisFile.isEmpty() && !QFile::remove(thisFile)) + LOG(VB_GENERAL, LOG_ERR, SLOC + + QString("Unable to delete %1.").arg(thisFile)); + + thisFile = hls->GetHTMLPageName(); + if (!thisFile.isEmpty() && !QFile::remove(thisFile)) + LOG(VB_GENERAL, LOG_ERR, SLOC + + QString("Unable to delete %1.").arg(thisFile)); + + query.prepare( + "DELETE FROM livestream " + "WHERE id = :STREAMID; "); + query.bindValue(":STREAMID", id); + + if (!query.exec()) + LOG(VB_RECORD, LOG_ERR, "Error deleting stream info in RemoveStream"); + + delete hls; + return true; +} + +DTC::LiveStreamInfo *HTTPLiveStream::StopStream(int id) +{ + MSqlQuery query(MSqlQuery::InitCon()); + query.prepare( + "UPDATE livestream " + "SET status = :STATUS " + "WHERE id = :STREAMID; "); + query.bindValue(":STATUS", (int)kHLSStatusStopping); + query.bindValue(":STREAMID", id); + + if (!query.exec()) + { + LOG(VB_GENERAL, LOG_ERR, SLOC + + QString("Unable to remove mark stream stopped for stream %1.") + .arg(id)); + return false; + } + + HTTPLiveStream *hls = new HTTPLiveStream(id); + if (!hls) + return NULL; + + MythTimer statusTimer; + int delay = 250000; + statusTimer.start(); + + HTTPLiveStreamStatus status = hls->GetDBStatus(); + while ((status != kHLSStatusStopped) && + (status != kHLSStatusCompleted) && + (status != kHLSStatusErrored) && + ((statusTimer.elapsed() / 1000) < 30)) + { + delay = (int)(delay * 1.5); + usleep(delay); + + status = hls->GetDBStatus(); + } + + hls->LoadFromDB(); + DTC::LiveStreamInfo *pLiveStreamInfo = hls->GetLiveStreamInfo(); + + delete hls; + return pLiveStreamInfo; +} + +///////////////////////////////////////////////////////////////////////////// +// Content Service API helpers +///////////////////////////////////////////////////////////////////////////// + +DTC::LiveStreamInfo *HTTPLiveStream::GetLiveStreamInfo( + DTC::LiveStreamInfo *info) +{ + if (!info) + info = new DTC::LiveStreamInfo(); + + info->setId((int)m_streamid); + info->setWidth((int)m_width); + info->setHeight((int)m_height); + info->setBitrate((int)m_bitrate); + info->setAudioBitrate((int)m_audioBitrate); + info->setSegmentSize((int)m_segmentSize); + info->setMaxSegments((int)m_maxSegments); + info->setStartSegment((int)m_startSegment); + info->setCurrentSegment((int)m_curSegment); + info->setSegmentCount((int)m_segmentCount); + info->setPercentComplete((int)m_percentComplete); + info->setCreated(m_created); + info->setLastModified(m_lastModified); + info->setRelativeURL(m_relativeURL); + info->setFullURL(m_fullURL); + info->setStatusStr(StatusToString(m_status)); + info->setStatusInt((int)m_status); + info->setStatusMessage(m_statusMessage); + info->setSourceFile(m_sourceFile); + info->setSourceHost(m_sourceHost); + info->setSourceWidth(m_sourceWidth); + info->setSourceHeight(m_sourceHeight); + info->setAudioOnlyBitrate((int)m_audioOnlyBitrate); + + return info; +} + +DTC::LiveStreamInfoList *HTTPLiveStream::GetLiveStreamInfoList(void) +{ + DTC::LiveStreamInfoList *infoList = new DTC::LiveStreamInfoList(); + + MSqlQuery query(MSqlQuery::InitCon()); + query.prepare( + "SELECT id " + "FROM livestream " + "ORDER BY lastmodified DESC;"); + + if (!query.exec()) + { + LOG(VB_GENERAL, LOG_ERR, SLOC + "Unable to get list of Live Streams"); + return infoList; + } + + DTC::LiveStreamInfo *info = NULL; + HTTPLiveStream *hls = NULL; + while (query.next()) + { + hls = new HTTPLiveStream(query.value(0).toUInt()); + info = infoList->AddNewLiveStreamInfo(); + hls->GetLiveStreamInfo(info); + delete hls; + } + + return infoList; +} + +/* vim: set expandtab tabstop=4 shiftwidth=4: */ + diff --git a/mythtv/libs/libmythtv/httplivestream.h b/mythtv/libs/libmythtv/httplivestream.h new file mode 100644 index 00000000000..f5cc902ddbe --- /dev/null +++ b/mythtv/libs/libmythtv/httplivestream.h @@ -0,0 +1,117 @@ +#ifndef HTTPLIVESTREAM_H +#define HTTPLIVESTREAM_H + +#include + +#include "datacontracts/liveStreamInfoList.h" + +#include "frame.h" + +typedef enum { + kHLSStatusUndefined = -1, + kHLSStatusQueued = 0, + kHLSStatusStarting = 1, + kHLSStatusRunning = 2, + kHLSStatusCompleted = 3, + kHLSStatusErrored = 4, + kHLSStatusStopping = 5, + kHLSStatusStopped = 6 +} HTTPLiveStreamStatus; + + +class HTTPLiveStream +{ + public: + HTTPLiveStream(QString srcFile, uint16_t width = 640, uint16_t height = 480, + uint32_t bitrate = 800000, uint32_t abitrate = 64000, + uint16_t maxSegments = 0, uint16_t segmentSize = 10, + uint32_t aobitrate = 32000, uint16_t srate = -1); + HTTPLiveStream(int streamid); + ~HTTPLiveStream(); + + bool InitForWrite(void); + bool LoadFromDB(void); + + int GetStreamID(void) { return m_streamid; } + uint16_t GetWidth(void) { return m_width; } + uint16_t GetHeight(void) { return m_height; } + uint32_t GetBitrate(void) { return m_bitrate; } + uint32_t GetAudioBitrate(void) { return m_audioBitrate; } + uint32_t GetAudioOnlyBitrate(void) { return m_audioOnlyBitrate; } + uint16_t GetMaxSegments(void) { return m_maxSegments; } + QString GetSourceFile(void) { return m_sourceFile; } + QString GetHTMLPageName(void); + QString GetMetaPlaylistName(void); + QString GetPlaylistName(bool audioOnly = false); + uint16_t GetSegmentSize(void) { return m_segmentSize; } + QString GetFilename(uint16_t segmentNumber = 0, bool fileOnly = false, + bool audioOnly = false); + QString GetCurrentFilename(bool audioOnly = false); + + HTTPLiveStreamStatus GetDBStatus(void); + + int AddStream(void); + bool AddSegment(void); + + bool WriteHTML(void); + bool WriteMetaPlaylist(void); + bool WritePlaylist(bool audioOnly = false, bool writeEndTag = false); + + bool SaveSegmentInfo(void); + + bool UpdateSizeInfo(uint16_t width, uint16_t height, + uint16_t srcwidth, uint16_t srcheight); + bool UpdateStatus(HTTPLiveStreamStatus status); + bool UpdateStatusMessage(QString message); + bool UpdatePercentComplete(int percent); + + QString StatusToString(HTTPLiveStreamStatus status); + + bool CheckStop(void); + + DTC::LiveStreamInfo *StartStream(void); + static DTC::LiveStreamInfo *StopStream(int id); + static bool RemoveStream(int id); + + DTC::LiveStreamInfo *GetLiveStreamInfo(DTC::LiveStreamInfo *info = NULL); + static DTC::LiveStreamInfoList *GetLiveStreamInfoList(); + + protected: + bool m_writing; + int m_streamid; + QString m_sourceFile; + QString m_sourceHost; + uint16_t m_sourceWidth; + uint16_t m_sourceHeight; + QString m_outDir; + QString m_outBase; + QString m_outFile; + QString m_audioOutFile; + uint16_t m_segmentSize; + uint16_t m_segmentFrames; + uint16_t m_maxSegments; + uint16_t m_segmentCount; + uint16_t m_startSegment; + uint16_t m_curSegment; + QString m_httpPrefix; + uint16_t m_height; + uint16_t m_width; + uint32_t m_bitrate; + uint32_t m_audioBitrate; + uint32_t m_audioOnlyBitrate; + uint16_t m_sampleRate; + + QDateTime m_created; + QDateTime m_lastModified; + uint16_t m_percentComplete; + QString m_relativeURL; + QString m_fullURL; + QString m_statusMessage; + + HTTPLiveStreamStatus m_status; +}; + +#endif + +/* vim: set expandtab tabstop=4 shiftwidth=4: */ + diff --git a/mythtv/libs/libmythtv/libmythtv.pro b/mythtv/libs/libmythtv/libmythtv.pro index 748c1edb4a5..3dc0294f6ec 100644 --- a/mythtv/libs/libmythtv/libmythtv.pro +++ b/mythtv/libs/libmythtv/libmythtv.pro @@ -35,6 +35,7 @@ DEPENDPATH += ../libmythlivemedia/UsageEnvironment/include DEPENDPATH += ../libmythlivemedia/UsageEnvironment DEPENDPATH += ../libmythbase ../libmythui DEPENDPATH += ../libmythupnp +DEPENDPATH += ../libmythservicecontracts INCLUDEPATH += .. ../.. # for avlib headers INCLUDEPATH += ../../external/FFmpeg @@ -202,6 +203,14 @@ SOURCES += diseqc.cpp diseqcsettings.cpp HEADERS += datadirect.h SOURCES += datadirect.cpp +# File Writer classes +HEADERS += filewriterbase.h avformatwriter.h +SOURCES += filewriterbase.cpp avformatwriter.cpp + +# HTTP Live Streaming +HEADERS += httplivestream.h +SOURCES += httplivestream.cpp + # Teletext stuff HEADERS += teletextdecoder.h teletextreader.h vbilut.h SOURCES += teletextdecoder.cpp teletextreader.cpp vbilut.cpp diff --git a/mythtv/libs/libmythupnp/httprequest.cpp b/mythtv/libs/libmythupnp/httprequest.cpp index 75d63660eea..4b221087cba 100644 --- a/mythtv/libs/libmythupnp/httprequest.cpp +++ b/mythtv/libs/libmythupnp/httprequest.cpp @@ -88,7 +88,6 @@ static MIMETypes g_MIMETypes[] = { "mpg2", "video/mpeg" }, { "mpeg", "video/mpeg" }, { "mpeg2","video/mpeg" }, - { "ts" , "video/mpegts" }, { "vob" , "video/mpeg" }, { "asf" , "video/x-ms-asf" }, { "nuv" , "video/nupplevideo" }, @@ -106,6 +105,9 @@ static MIMETypes g_MIMETypes[] = // Similarly, this could be audio/flac or application/flac: { "flac", "audio/x-flac" }, { "m4a" , "audio/x-m4a" }, + // HTTP Live Streaming + { "m3u8", "application/x-mpegurl" }, + { "ts" , "video/mp2t" }, }; static const char *Static401Error = diff --git a/mythtv/programs/mythbackend/services/content.cpp b/mythtv/programs/mythbackend/services/content.cpp index 06cdb612247..988623b14ed 100644 --- a/mythtv/programs/mythbackend/services/content.cpp +++ b/mythtv/programs/mythbackend/services/content.cpp @@ -37,6 +37,7 @@ #include "util.h" #include "mythdownloadmanager.h" #include "metadataimagehelper.h" +#include "httplivestream.h" ///////////////////////////////////////////////////////////////////////////// // @@ -706,6 +707,10 @@ QFileInfo Content::GetVideo( int nId ) return QFileInfo( sFileName ); } +///////////////////////////////////////////////////////////////////////////// +// +///////////////////////////////////////////////////////////////////////////// + QString Content::GetHash( const QString &sStorageGroup, const QString &sFileName ) { @@ -735,6 +740,10 @@ QString Content::GetHash( const QString &sStorageGroup, return hash; } +///////////////////////////////////////////////////////////////////////////// +// +///////////////////////////////////////////////////////////////////////////// + bool Content::DownloadFile( const QString &sURL, const QString &sStorageGroup ) { QFileInfo finfo(sURL); @@ -768,3 +777,246 @@ bool Content::DownloadFile( const QString &sURL, const QString &sStorageGroup ) return false; } +///////////////////////////////////////////////////////////////////////////// +// +///////////////////////////////////////////////////////////////////////////// + +DTC::LiveStreamInfo *Content::AddLiveStream( const QString &sStorageGroup, + const QString &sFileName, + const QString &sHostName, + const QString &sMaxSegments, + const QString &sWidth, + const QString &sHeight, + const QString &sBitrate, + const QString &sAudioBitrate, + const QString &sSampleRate ) +{ + QString sGroup = sStorageGroup; + + if (sGroup.isEmpty()) + { + LOG(VB_UPNP, LOG_WARNING, + "AddLiveStream - StorageGroup missing... using 'Default'"); + sGroup = "Default"; + } + + if (sFileName.isEmpty()) + { + QString sMsg ( "AddLiveStream - FileName missing." ); + + LOG(VB_UPNP, LOG_ERR, sMsg); + + throw sMsg; + } + + // ------------------------------------------------------------------ + // Search for the filename + // ------------------------------------------------------------------ + + QString sFullFileName; + if (sHostName.isEmpty() || sHostName == gCoreContext->GetHostName()) + { + StorageGroup storage( sGroup ); + sFullFileName = storage.FindFile( sFileName ); + + if (sFullFileName.isEmpty()) + { + LOG(VB_UPNP, LOG_ERR, + QString("AddLiveStream - Unable to find %1.").arg(sFileName)); + + return NULL; + } + } + else + { + sFullFileName = + gCoreContext->GenMythURL(sHostName, 0, sFileName, sStorageGroup); + } + + uint16_t nWidth = 480; + uint16_t nHeight = 0; + uint32_t nBitrate = 800000; + uint32_t nAudioBitrate = 64000; + uint16_t nMaxSegments = 0; + uint16_t nSampleRate = -1; + + bool ok = false; + uint32_t value = 0; + + if (!sWidth.isEmpty()) + { + value = sWidth.toUInt(&ok); + if (ok) + nWidth = value; + } + + if (!sHeight.isEmpty()) + { + value = sHeight.toUInt(&ok); + if (ok) + nHeight = value; + } + + if (!sBitrate.isEmpty()) + { + value = sBitrate.toUInt(&ok); + if (ok) + nBitrate = value; + } + + if (!sAudioBitrate.isEmpty()) + { + value = sAudioBitrate.toUInt(&ok); + if (ok) + nAudioBitrate = value; + } + + if (!sMaxSegments.isEmpty()) + { + value = sMaxSegments.toUInt(&ok); + if (ok) + nMaxSegments = value; + } + + if (!sSampleRate.isEmpty()) + { + value = sSampleRate.toUInt(&ok); + if (ok) + nSampleRate = value; + } + + HTTPLiveStream *hls = new + HTTPLiveStream(sFullFileName, nWidth, nHeight, nBitrate, nAudioBitrate, + nMaxSegments, 10, 32000, nSampleRate); + + if (!hls) + { + LOG(VB_UPNP, LOG_ERR, + "AddLiveStream - Unable to create HTTPLiveStream."); + return NULL; + } + + DTC::LiveStreamInfo *lsInfo = hls->StartStream(); + + delete hls; + + return lsInfo; +} + +///////////////////////////////////////////////////////////////////////////// +// +///////////////////////////////////////////////////////////////////////////// + +bool Content::RemoveLiveStream( int nId ) +{ + return HTTPLiveStream::RemoveStream(nId); +} + +///////////////////////////////////////////////////////////////////////////// +// +///////////////////////////////////////////////////////////////////////////// + +DTC::LiveStreamInfo *Content::StopLiveStream( int nId ) +{ + return HTTPLiveStream::StopStream(nId); +} + +///////////////////////////////////////////////////////////////////////////// +// +///////////////////////////////////////////////////////////////////////////// + +DTC::LiveStreamInfo *Content::GetLiveStream( int nId ) +{ + HTTPLiveStream *hls = new HTTPLiveStream(nId); + + if (!hls) + { + LOG( VB_UPNP, LOG_ERR, + QString("GetLiveStream - for stream id %1 failed").arg( nId )); + return NULL; + } + + DTC::LiveStreamInfo *hlsInfo = hls->GetLiveStreamInfo(); + if (!hlsInfo) + { + LOG( VB_UPNP, LOG_ERR, + QString("HLS::GetLiveStreamInfo - for stream id %1 failed") + .arg( nId )); + return NULL; + } + + delete hls; + return hlsInfo; +} + +///////////////////////////////////////////////////////////////////////////// +// +///////////////////////////////////////////////////////////////////////////// +DTC::LiveStreamInfoList *Content::GetLiveStreamList( void ) +{ + return HTTPLiveStream::GetLiveStreamInfoList(); +} + +///////////////////////////////////////////////////////////////////////////// +// +///////////////////////////////////////////////////////////////////////////// +DTC::LiveStreamInfo *Content::AddRecordingLiveStream( int nChanId, + const QDateTime &dtStartTime, + const QString &sMaxSegments, + const QString &sWidth, + const QString &sHeight, + const QString &sBitrate, + const QString &sAudioBitrate, + const QString &sSampleRate ) +{ + if (!dtStartTime.isValid()) + throw( "StartTime is invalid" ); + + // ------------------------------------------------------------------ + // Read Recording From Database + // ------------------------------------------------------------------ + + ProgramInfo pginfo( (uint)nChanId, dtStartTime ); + + if (!pginfo.GetChanID()) + { + LOG( VB_UPNP, LOG_ERR, QString("AddRecordingLiveStream - for %1, %2 failed") + .arg( nChanId ) + .arg( dtStartTime.toString() )); + return NULL; + } + + if ( pginfo.GetHostname() != gCoreContext->GetHostName()) + { + // We only handle requests for local resources + + QString sMsg = + QString("GetRecording: Wrong Host '%1' request from '%2'.") + .arg( gCoreContext->GetHostName()) + .arg( pginfo.GetHostname() ); + + LOG(VB_UPNP, LOG_ERR, sMsg); + + throw HttpRedirectException( pginfo.GetHostname() ); + } + + QString sFileName( GetPlaybackURL(&pginfo) ); + + // ---------------------------------------------------------------------- + // check to see if the file exists + // ---------------------------------------------------------------------- + + if (!QFile::exists( sFileName )) + { + LOG( VB_UPNP, LOG_ERR, QString("AddRecordingLiveStream - for %1, %2 failed") + .arg( nChanId ) + .arg( dtStartTime.toString() )); + return NULL; + } + + QFileInfo fInfo( sFileName ); + + return AddLiveStream( pginfo.GetStorageGroup(), fInfo.fileName(), + pginfo.GetHostname(), sMaxSegments, sWidth, + sHeight, sBitrate, sAudioBitrate, sSampleRate ); +} diff --git a/mythtv/programs/mythbackend/services/content.h b/mythtv/programs/mythbackend/services/content.h index 27700277dc4..201e16e73a7 100644 --- a/mythtv/programs/mythbackend/services/content.h +++ b/mythtv/programs/mythbackend/services/content.h @@ -81,6 +81,31 @@ class Content : public ContentServices bool DownloadFile ( const QString &URL, const QString &StorageGroup ); + // HTTP Live Streaming + 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 ); + + 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 ); + + DTC::LiveStreamInfo *GetLiveStream ( int Id ); + DTC::LiveStreamInfoList *GetLiveStreamList ( void ); + + DTC::LiveStreamInfo *StopLiveStream ( int Id ); + bool RemoveLiveStream ( int Id ); }; Q_SCRIPT_DECLARE_QMETAOBJECT( Content, QObject*); diff --git a/mythtv/programs/mythtranscode/commandlineparser.cpp b/mythtv/programs/mythtranscode/commandlineparser.cpp index 14fe6a20bc1..b931478c3da 100644 --- a/mythtv/programs/mythtranscode/commandlineparser.cpp +++ b/mythtv/programs/mythtranscode/commandlineparser.cpp @@ -39,6 +39,10 @@ void MythTranscodeCommandLineParser::LoadArguments(void) add(QStringList( QStringList() << "-e" << "--ostream" ), "ostream", "", "Output stream type: dvd, ps", "") ->SetGroup("Encoding"); +// add("--avf", "avf", false, "Generate libavformat output file.", "") +// ->SetGroup("Encoding"); + add("--hls", "hls", false, "Generate HTTP Live Stream output.", "") + ->SetGroup("Encoding"); add(QStringList( QStringList() << "-f" << "--fifodir" ), "fifodir", "", "Directory in which to write fifos to.", "") @@ -77,5 +81,30 @@ void MythTranscodeCommandLineParser::LoadArguments(void) "Add a new transcoding job of the specified recording and " "profile to the jobqueue. Accepts an optional string to define " "the hostname.", ""); + +// add("--container", "container", "", "Output file container format", "") +// ->SetChildOf("avf"); +// add("--acodec", "acodec", "", "Output file audio codec", "") +// ->SetChildOf("avf"); +// add("--vcodec", "vcodec", "", "Output file video codec", "") +// ->SetChildOf("avf"); + add("--width", "width", 0, "Output Video Width", "") +// ->SetChildOf("avf") + ->SetChildOf("hls"); + add("--height", "height", 0, "Output Video Height", "") +// ->SetChildOf("avf") + ->SetChildOf("hls"); + add("--bitrate", "bitrate", 800, "Output Video Bitrate (Kbits)", "") +// ->SetChildOf("avf") + ->SetChildOf("hls"); + add("--audiobitrate", "audiobitrate", 64, "Output Audio Bitrate (Kbits)", "") +// ->SetChildOf("avf") + ->SetChildOf("hls"); + add("--maxsegments", "maxsegments", 0, "Max HTTP Live Stream segments", "") + ->SetChildOf("hls"); + add("--noaudioonly", "noaudioonly", 0, "Disable Audio-Only HLS Stream", "") + ->SetChildOf("hls"); + add("--hlsstreamid", "hlsstreamid", -1, "Stream ID to process", "") + ->SetChildOf("hls"); } diff --git a/mythtv/programs/mythtranscode/main.cpp b/mythtv/programs/mythtranscode/main.cpp index 9ef35d4ac98..a12af9b1925 100644 --- a/mythtv/programs/mythtranscode/main.cpp +++ b/mythtv/programs/mythtranscode/main.cpp @@ -237,7 +237,7 @@ int main(int argc, char *argv[]) useCutlist = true; if (!cmdline.toString("usecutlist").isEmpty()) { - if (!cmdline.toBool("inputfile")) + if (!cmdline.toBool("inputfile") && !cmdline.toBool("hls")) { cerr << "External cutlists are only allowed when using" << endl << "the --infile option." << endl; @@ -356,13 +356,14 @@ int main(int argc, char *argv[]) } } - if ((!found_infile && !(found_chanid && found_starttime)) || - (found_infile && (found_chanid || found_starttime)) ) + if (((!found_infile && !(found_chanid && found_starttime)) || + (found_infile && (found_chanid || found_starttime))) && + (!cmdline.toBool("hls"))) { cerr << "Must specify -i OR -c AND -s options!" << endl; return GENERIC_EXIT_INVALID_CMDLINE; } - if (isVideo && !found_infile) + if (isVideo && !found_infile && !cmdline.toBool("hls")) { cerr << "Must specify --infile to use --video" << endl; return GENERIC_EXIT_INVALID_CMDLINE; @@ -408,7 +409,11 @@ int main(int argc, char *argv[]) } ProgramInfo *pginfo = NULL; - if (isVideo) + if (cmdline.toBool("hls")) + { + pginfo = new ProgramInfo(); + } + else if (isVideo) { // We want the absolute file path for the filemarkup table QFileInfo inf(infile); @@ -456,7 +461,7 @@ int main(int argc, char *argv[]) } if (infile.left(7) == "myth://" && (outfile.isEmpty() || outfile != "-") && - fifodir.isEmpty()) + fifodir.isEmpty() && !cmdline.toBool("hls") && !cmdline.toBool("avf")) { LOG(VB_GENERAL, LOG_ERR, QString("Attempted to transcode %1. Mythtranscode is currently " @@ -474,7 +479,11 @@ int main(int argc, char *argv[]) if (!build_index) { - if (fifodir.isEmpty()) + if (cmdline.toBool("hlsstreamid")) + LOG(VB_GENERAL, LOG_NOTICE, + QString("Transcoding HTTP Live Stream ID %1") + .arg(cmdline.toInt("hlsstreamid"))); + else if (fifodir.isEmpty()) LOG(VB_GENERAL, LOG_NOTICE, QString("Transcoding from %1 to %2") .arg(infile).arg(outfile)); else @@ -482,12 +491,47 @@ int main(int argc, char *argv[]) .arg(infile)); } + if (cmdline.toBool("avf")) + { + transcode->SetAVFMode(); + + if (cmdline.toBool("container")) + transcode->SetCMDContainer(cmdline.toString("container")); + if (cmdline.toBool("acodec")) + transcode->SetCMDAudioCodec(cmdline.toString("acodec")); + if (cmdline.toBool("vcodec")) + transcode->SetCMDVideoCodec(cmdline.toString("vcodec")); + } + else if (cmdline.toBool("hls")) + { + transcode->SetHLSMode(); + + if (cmdline.toBool("hlsstreamid")) + transcode->SetHLSStreamID(cmdline.toInt("hlsstreamid")); + if (cmdline.toBool("maxsegments")) + transcode->SetHLSMaxSegments(cmdline.toInt("maxsegments")); + if (cmdline.toBool("noaudioonly")) + transcode->DisableAudioOnlyHLS(); + } + + if (cmdline.toBool("avf") || cmdline.toBool("hls")) + { + if (cmdline.toBool("width")) + transcode->SetCMDWidth(cmdline.toInt("width")); + if (cmdline.toBool("height")) + transcode->SetCMDHeight(cmdline.toInt("height")); + if (cmdline.toBool("bitrate")) + transcode->SetCMDBitrate(cmdline.toInt("bitrate") * 1000); + if (cmdline.toBool("audiobitrate")) + transcode->SetCMDAudioBitrate(cmdline.toInt("audiobitrate") * 1000); + } + if (showprogress) transcode->ShowProgress(true); if (!recorderOptions.isEmpty()) transcode->SetRecorderOptions(recorderOptions); int result = 0; - if (!mpeg2 && !build_index) + if ((!mpeg2 && !build_index) || cmdline.toBool("hls")) { result = transcode->TranscodeFile(infile, outfile, profilename, useCutlist, diff --git a/mythtv/programs/mythtranscode/transcode.cpp b/mythtv/programs/mythtranscode/transcode.cpp index 84584aa69d4..d93d860038d 100644 --- a/mythtv/programs/mythtranscode/transcode.cpp +++ b/mythtv/programs/mythtranscode/transcode.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include "mythconfig.h" @@ -14,11 +16,14 @@ #include "mythcorecontext.h" #include "jobqueue.h" #include "exitcodes.h" +#include "mthreadpool.h" #include "NuppelVideoRecorder.h" #include "mythplayer.h" #include "programinfo.h" #include "mythdbcon.h" +#include "avformatwriter.h" +#include "httplivestream.h" extern "C" { @@ -50,6 +55,8 @@ class AudioReencodeBuffer : public AudioOutput ab_size = 128; ab = new AudioBuffer[ab_size]; m_initpassthru = passthru; + + m_audioFrameSize = 0; } ~AudioReencodeBuffer() @@ -110,20 +117,64 @@ class AudioReencodeBuffer : public AudioOutput ab = tmp; ab_size += 128; } - ab[ab_count].len = len; - ab[ab_count].offset = audiobuffer_len; - memcpy(audiobuffer + audiobuffer_len, buffer, - len); - audiobuffer_len += len; - audiobuffer_frames += frames; + if (m_audioFrameSize) + { + int inputOffset = 0; + if ((ab_count) && + (ab[ab_count-1].len != m_audioFrameSize)) + { + int copyLen = m_audioFrameSize - ab[ab_count-1].len; + if (copyLen > len) + copyLen = len; + + memcpy(audiobuffer + audiobuffer_len, buffer, + copyLen); + inputOffset += copyLen; + + ab[ab_count-1].len += copyLen; + ab[ab_count-1].time += (copyLen / bytes_per_frame / (eff_audiorate / 1000)); + audiobuffer_len += copyLen; + } + + while (inputOffset < len) + { + int copyLen = m_audioFrameSize; + if (copyLen > (len - inputOffset)) + copyLen = (len - inputOffset); + + memcpy(audiobuffer + audiobuffer_len, (char *)buffer + inputOffset, + copyLen); + inputOffset += copyLen; + + last_audiotime = timecode + ((inputOffset / bytes_per_frame) / (eff_audiorate / 1000)); + + ab[ab_count].len = copyLen; + ab[ab_count].offset = audiobuffer_len; + ab[ab_count].time = last_audiotime; + + audiobuffer_len += copyLen; + + ++ab_count; + } + } + else + { + ab[ab_count].len = len; + ab[ab_count].offset = audiobuffer_len; + + memcpy(audiobuffer + audiobuffer_len, buffer, + len); + audiobuffer_len += len; + audiobuffer_frames += frames; - // last_audiotime is at the end of the frame - last_audiotime = timecode + frames * 1000 / - eff_audiorate; + // last_audiotime is at the end of the frame + last_audiotime = timecode + frames * 1000 / + eff_audiorate; - ab[ab_count].time = last_audiotime; - ab_count++; + ab[ab_count].time = last_audiotime; + ab_count++; + } return true; } @@ -253,10 +304,137 @@ class AudioReencodeBuffer : public AudioOutput int channels, bits, bytes_per_frame, eff_audiorate; long long last_audiotime; bool m_passthru; + int m_audioFrameSize; private: bool m_initpassthru; }; +typedef struct transcodeFrameInfo +{ + VideoFrame *frame; + int didFF; + bool isKey; +} TranscodeFrameInfo; + +class TranscodeFrameQueue : public QRunnable +{ + public: + TranscodeFrameQueue(MythPlayer *player, VideoOutput *videoout, + bool cutlist, int size = 5) + : m_player(player), m_videoOutput(videoout), + m_honorCutlist(cutlist), + m_eof(false), m_maxFrames(size), + m_runThread(true), m_isRunning(false) + { + + } + + ~TranscodeFrameQueue() + { + m_runThread = false; + m_frameWaitCond.wakeAll(); + + while (m_isRunning) + usleep(50000); + } + + void stop(void) + { + m_runThread = false; + m_frameWaitCond.wakeAll(); + + while (m_isRunning) + usleep(50000); + } + + void run() + { + threadRegister("TranscodeFrameQueue"); + + frm_dir_map_t::iterator dm_iter; + + m_isRunning = true; + while (m_runThread) + { + if (m_frameList.size() < m_maxFrames) + { + TranscodeFrameInfo tfInfo; + tfInfo.frame = NULL; + tfInfo.didFF = 0; + tfInfo.isKey = false; + + if (m_player->TranscodeGetNextFrame(dm_iter, tfInfo.didFF, + tfInfo.isKey, m_honorCutlist)) + { + tfInfo.frame = m_videoOutput->GetLastDecodedFrame(); + + QMutexLocker locker(&m_queueLock); + m_frameList.append(tfInfo); + } + else + { + m_eof = true; + } + + m_frameWaitCond.wakeAll(); + } + else + { + m_frameWaitLock.lock(); + m_frameWaitCond.wait(&m_frameWaitLock); + m_frameWaitLock.unlock(); + } + } + m_isRunning = false; + + threadDeregister(); + } + + VideoFrame *GetFrame(int &didFF, bool &isKey) + { + if (m_eof) + return NULL; + + m_queueLock.lock(); + + if (m_frameList.isEmpty()) + { + m_queueLock.unlock(); + + m_frameWaitLock.lock(); + m_frameWaitCond.wait(&m_frameWaitLock); + m_frameWaitLock.unlock(); + + if (m_frameList.isEmpty()) + return NULL; + + m_queueLock.lock(); + } + + TranscodeFrameInfo tfInfo = m_frameList.takeFirst(); + m_queueLock.unlock(); + m_frameWaitCond.wakeAll(); + + didFF = tfInfo.didFF; + isKey = tfInfo.isKey; + + return tfInfo.frame; + } + + private: + MythPlayer *m_player; + VideoOutput *m_videoOutput; + bool m_honorCutlist; + bool m_eof; + int m_maxFrames; + bool m_runThread; + bool m_isRunning; + QMutex m_queueLock; + QList m_frameList; + QWaitCondition m_frameWaitCond; + QMutex m_frameWaitLock; +}; + Transcode::Transcode(ProgramInfo *pginfo) : m_proginfo(pginfo), keyframedist(30), @@ -268,7 +446,14 @@ Transcode::Transcode(ProgramInfo *pginfo) : fifow(NULL), kfa_table(NULL), showprogress(false), - recorderOptions("") + recorderOptions(""), + avfMode(false), + hlsMode(false), hlsStreamID(-1), + hlsMaxSegments(0), + cmdContainer("mpegts"), cmdAudioCodec("libmp3lame"), + cmdVideoCodec("libx264"), + cmdWidth(480), cmdHeight(0), + cmdBitrate(800000), cmdAudioBitrate(64000) { } @@ -412,16 +597,49 @@ int Transcode::TranscodeFile(const QString &inputname, { QDateTime curtime = QDateTime::currentDateTime(); QDateTime statustime = curtime; - int audioframesize; int audioFrame = 0; + AVFormatWriter *avfw = NULL; + AVFormatWriter *avfw2 = NULL; + HTTPLiveStream *hls = NULL; + int hlsSegmentSize = 0; + int hlsSegmentFrames = 0; if (jobID >= 0) JobQueue::ChangeJobComment(jobID, "0% " + QObject::tr("Completed")); - nvr = new NuppelVideoRecorder(NULL, NULL); + if (hlsMode) + { + avfMode = true; + + if (hlsStreamID != -1) + { + hls = new HTTPLiveStream(hlsStreamID); + hls->UpdateStatus(kHLSStatusStarting); + cmdWidth = hls->GetWidth(); + cmdHeight = hls->GetHeight(); + cmdBitrate = hls->GetBitrate(); + cmdAudioBitrate = hls->GetAudioBitrate(); + } + } + + if (!avfMode) + { + nvr = new NuppelVideoRecorder(NULL, NULL); + + if (!nvr) + { + LOG(VB_GENERAL, LOG_ERR, + "Transcoding aborted, error creating NuppelVideoRecorder."); + return REENCODE_ERROR; + } + } // Input setup - inRingBuffer = RingBuffer::Create(inputname, false, false); + if (hls && (hlsStreamID != -1)) + inRingBuffer = RingBuffer::Create(hls->GetSourceFile(), false, false); + else + inRingBuffer = RingBuffer::Create(inputname, false, false); + player = new MythPlayer(); player_ctx = new PlayerContext(kTranscoderInUseID); @@ -533,10 +751,185 @@ int Transcode::TranscodeFile(const QString &inputname, float video_frame_rate = player->GetFrameRate(); int newWidth = video_width; int newHeight = video_height; + bool halfFramerate = false; + bool skippedLastFrame = false; kfa_table = new vector; - if (fifodir.isEmpty()) + if (avfMode) + { + newWidth = cmdWidth; + newHeight = cmdHeight; + + int actualHeight = (video_height == 1088 ? 1080 : video_height); + float newAspect = 1.0 * video_width / actualHeight; + + if (newAspect < 1.3) + newAspect = video_aspect; + + // If height or width are 0, then we need to calculate them + if (newHeight == 0 && newWidth > 0) + newHeight = (int)(1.0 * newWidth / newAspect); + else if (newWidth == 0 && newHeight > 0) + newWidth = (int)(1.0 * newHeight * newAspect); + else if (newWidth == 0 && newHeight == 0) + { + newHeight = 480; + newWidth = (int)(1.0 * 480 * newAspect); + if (newWidth > 640) + { + newWidth = 640; + newHeight = (int)(1.0 * 640 / newAspect); + } + } + + // make sure dimensions are valid for MPEG codecs + newHeight = (newHeight + 15) & ~0xF; + newWidth = (newWidth + 15) & ~0xF; + + LOG(VB_RECORD, LOG_ERR, "Configuring AVFormatWriter"); + avfw = new AVFormatWriter(); + if (!avfw) + { + LOG(VB_GENERAL, LOG_ERR, + "Transcoding aborted, error creating AVFormatWriter."); + if (player_ctx) + delete player_ctx; + return REENCODE_ERROR; + } + + avfw->SetVideoBitrate(cmdBitrate); + avfw->SetHeight(newHeight); + avfw->SetWidth(newWidth); + avfw->SetAspect(player->GetVideoAspect()); + avfw->SetAudioBitrate(cmdAudioBitrate); + avfw->SetAudioChannels(arb->channels); + avfw->SetAudioBits(16); + avfw->SetAudioSampleRate(arb->eff_audiorate); + avfw->SetAudioSampleBytes(2); + + if (hlsMode) + { + int segmentSize = 10; + int audioOnlyBitrate = 0; + + if (!hlsDisableAudioOnly) + { + audioOnlyBitrate = 48000; + + avfw2 = new AVFormatWriter(); + if (!avfw2) + { + LOG(VB_GENERAL, LOG_ERR, "Transcoding aborted, error " + "creating low-bitrate AVFormatWriter."); + if (player_ctx) + delete player_ctx; + return REENCODE_ERROR; + } + + avfw2->SetContainer("mpegts"); + avfw2->SetAudioCodec("libmp3lame"); + avfw2->SetAudioBitrate(audioOnlyBitrate); + avfw2->SetAudioChannels(arb->channels); + avfw2->SetAudioBits(16); + avfw2->SetAudioSampleRate(arb->eff_audiorate); + avfw2->SetAudioSampleBytes(2); + } + + avfw->SetContainer("mpegts"); + avfw->SetVideoCodec("libx264"); + avfw->SetAudioCodec("libmp3lame"); + //avfw->SetAudioCodec("libfaac"); + + if (hlsStreamID == -1) + hls = new HTTPLiveStream(inputname, newWidth, newHeight, cmdBitrate, + cmdAudioBitrate, hlsMaxSegments, + segmentSize, audioOnlyBitrate); + + hls->UpdateStatus(kHLSStatusStarting); + hls->UpdateSizeInfo(newWidth, newHeight, video_width, video_height); + + if (hlsStreamID != -1) + hls->InitForWrite(); + + if (video_frame_rate > 30) + { + halfFramerate = true; + avfw->SetFramerate(video_frame_rate/2); + + if (avfw2) + avfw2->SetFramerate(video_frame_rate/2); + + hlsSegmentSize = (int)(segmentSize * video_frame_rate / 2); + } + else + { + avfw->SetFramerate(video_frame_rate); + + if (avfw2) + avfw2->SetFramerate(video_frame_rate); + + hlsSegmentSize = (int)(segmentSize * video_frame_rate); + } + + avfw->SetKeyFrameDist(90); + avfw2->SetKeyFrameDist(90); + + hls->AddSegment(); + avfw->SetFilename(hls->GetCurrentFilename()); + if (avfw2) + avfw2->SetFilename(hls->GetCurrentFilename(true)); + } + else + { + avfw->SetContainer(cmdContainer); + avfw->SetVideoCodec(cmdVideoCodec); + avfw->SetAudioCodec(cmdAudioCodec); + avfw->SetFilename(outputname); + avfw->SetFramerate(video_frame_rate); + } + + avfw->SetThreadCount(2); + if (avfw2) + avfw2->SetThreadCount(1); + + if (!avfw->Init()) + { + LOG(VB_RECORD, LOG_ERR, "avfw->Init() failed"); + if (player_ctx) + delete player_ctx; + return REENCODE_ERROR; + } + + if (!avfw->OpenFile()) + { + LOG(VB_RECORD, LOG_ERR, "avfw->OpenFile() failed"); + if (player_ctx) + delete player_ctx; + return REENCODE_ERROR; + } + + if (avfw2 && !avfw2->Init()) + { + LOG(VB_RECORD, LOG_ERR, "avfw2->Init() failed"); + if (player_ctx) + delete player_ctx; + return REENCODE_ERROR; + } + + if (avfw2 && !avfw2->OpenFile()) + { + LOG(VB_RECORD, LOG_ERR, "avfw2->OpenFile() failed"); + if (player_ctx) + delete player_ctx; + return REENCODE_ERROR; + } + + arb->m_audioFrameSize = avfw->GetAudioFrameSize() * arb->channels * 2; + + player->SetVideoFilters(gCoreContext->GetSetting("HTTPLiveStreamFilters")); + } + else if (fifodir.isEmpty()) { if (!GetProfile(profileName, encodingType, video_height, (int)round(video_frame_rate))) { @@ -752,7 +1145,7 @@ int Transcode::TranscodeFile(const QString &inputname, nvr->StreamAllocate(); } - if (vidsetting == encodingType && !framecontrol && + if (vidsetting == encodingType && !framecontrol && !avfMode && fifodir.isEmpty() && honorCutList && video_width == newWidth && video_height == newHeight) { @@ -763,7 +1156,6 @@ int Transcode::TranscodeFile(const QString &inputname, if (deleteMap.size() > 0) player->SetCutList(deleteMap); - keyframedist = 30; player->InitForTranscode(copyaudio, copyvideo); if (player->IsErrored()) { @@ -843,6 +1235,8 @@ int Transcode::TranscodeFile(const QString &inputname, // Request was for just the format of fifo data, not for // the actual transcode, so stop here. unlink(outputname.toLocal8Bit().constData()); + if (player_ctx) + delete player_ctx; return REENCODE_OK; } @@ -896,7 +1290,6 @@ int Transcode::TranscodeFile(const QString &inputname, bool is_key = 0; bool first_loop = true; unsigned char *newFrame = new unsigned char[frame.size]; - frame.buf = newFrame; AVPicture imageIn, imageOut; struct SwsContext *scontext = NULL; @@ -905,20 +1298,34 @@ int Transcode::TranscodeFile(const QString &inputname, LOG(VB_GENERAL, LOG_INFO, "Dumping Video and Audio data to fifos"); else if (copyaudio) LOG(VB_GENERAL, LOG_INFO, "Copying Audio while transcoding Video"); + else if (hlsMode) + LOG(VB_GENERAL, LOG_INFO, "Transcoding for HTTP Live Streaming"); + else if (avfMode) + LOG(VB_GENERAL, LOG_INFO, "Transcoding to libavformat container"); else LOG(VB_GENERAL, LOG_INFO, "Transcoding Video and Audio"); + TranscodeFrameQueue *frameQueue = + new TranscodeFrameQueue(player, videoOutput, honorCutList); + MThreadPool::globalInstance()->start(frameQueue, "TranscodeFrameQueue"); + QTime flagTime; flagTime.start(); - while (player->TranscodeGetNextFrame(dm_iter, did_ff, is_key, honorCutList)) + bool stopSignalled = false; + VideoFrame *lastDecode = NULL; + + if (hls) + hls->UpdateStatus(kHLSStatusRunning); + + while ((!stopSignalled) && (lastDecode = frameQueue->GetFrame(did_ff, is_key))) { if (first_loop) { copyaudio = player->GetRawAudioState(); first_loop = false; } - VideoFrame *lastDecode = videoOutput->GetLastDecodedFrame(); + float new_aspect = lastDecode->aspect; frame.timecode = lastDecode->timecode; @@ -1025,6 +1432,8 @@ int Transcode::TranscodeFile(const QString &inputname, delete [] newFrame; if (player_ctx) delete player_ctx; + if (frameQueue) + frameQueue->stop(); return REENCODE_ERROR; } @@ -1132,7 +1541,8 @@ int Transcode::TranscodeFile(const QString &inputname, if (video_aspect != new_aspect) { video_aspect = new_aspect; - nvr->SetNewVideoParams(video_aspect); + if (nvr) + nvr->SetNewVideoParams(video_aspect); } @@ -1174,54 +1584,158 @@ int Transcode::TranscodeFile(const QString &inputname, } // audio is fully decoded, so we need to reencode it - audioframesize = arb->audiobuffer_len; if (arb->ab_count) { - for (uint loop = 0; loop < arb->ab_count; loop++) + uint loop = 0; + int bytesConsumed = 0; + int buffersConsumed = 0; + for (loop = 0; loop < arb->ab_count; loop++) { - nvr->SetOption("audioframesize", arb->ab[loop].len); - nvr->WriteAudio(arb->audiobuffer + arb->ab[loop].offset, - audioFrame++, + if (arb->ab[loop].time > frame.timecode) + break; + + if (avfMode) + { + if (did_ff != 1) + { + avfw->WriteAudioFrame( + arb->audiobuffer + arb->ab[loop].offset, + audioFrame, + arb->ab[loop].time - timecodeOffset); + + if (avfw2) + { + if ((avfw2->GetTimecodeOffset() == -1) && + (avfw->GetTimecodeOffset() != -1)) + { + avfw2->SetTimecodeOffset( + avfw->GetTimecodeOffset()); + } + + avfw2->WriteAudioFrame( + arb->audiobuffer + arb->ab[loop].offset, + audioFrame, arb->ab[loop].time - timecodeOffset); - if (nvr->IsErrored()) + } + + ++audioFrame; + } + } + else + { + nvr->SetOption("audioframesize", arb->ab[loop].len); + nvr->WriteAudio(arb->audiobuffer + arb->ab[loop].offset, + audioFrame++, + arb->ab[loop].time - timecodeOffset); + if (nvr->IsErrored()) + { + LOG(VB_GENERAL, LOG_ERR, + "Transcode: Encountered irrecoverable error in " + "NVR::WriteAudio"); + + delete [] newFrame; + if (player_ctx) + delete player_ctx; + if (frameQueue) + frameQueue->stop(); + return REENCODE_ERROR; + } + } + + ++buffersConsumed; + bytesConsumed += arb->ab[loop].len; + } + + if (loop && (loop < arb->ab_count)) + { + int newCount = 0; + int newLen = 0; + int offset = 0; + int index = 0; + for (; loop < arb->ab_count; ++loop) { - LOG(VB_GENERAL, LOG_ERR, - "Transcode: Encountered irrecoverable error in " - "NVR::WriteAudio"); - - delete [] newFrame; - if (player_ctx) - delete player_ctx; - return REENCODE_ERROR; + memcpy(arb->audiobuffer + offset, + arb->audiobuffer + arb->ab[loop].offset, + arb->ab[loop].len); + arb->ab[loop].offset = offset; + offset += arb->ab[loop].len; + newCount++; + newLen += arb->ab[loop].len; + arb->ab[index].len = arb->ab[loop].len; + arb->ab[index].offset = arb->ab[loop].offset; + arb->ab[index].time = arb->ab[loop].time; + index++; } + + arb->ab_count = newCount; + arb->audiobuffer_len = newLen; } - arb->ab_count = 0; - arb->audiobuffer_len = 0; } - player->GetCC608Reader()->TranscodeWriteText(&TranscodeWriteText, - (void *)(nvr)); - + if (!avfMode) + player->GetCC608Reader()->TranscodeWriteText(&TranscodeWriteText, + (void *)(nvr)); lasttimecode = frame.timecode; frame.timecode -= timecodeOffset; - if (forceKeyFrames) - nvr->WriteVideo(&frame, true, true); + if (avfMode) + { + if (halfFramerate && !skippedLastFrame) + { + skippedLastFrame = true; + } + else + { + skippedLastFrame = false; + + if ((hls) && + (avfw->GetFramesWritten()) && + (hlsSegmentFrames > hlsSegmentSize) && + (avfw->NextFrameIsKeyFrame())) + { + hls->AddSegment(); + avfw->ReOpen(hls->GetCurrentFilename()); + + if (avfw2) + avfw2->ReOpen(hls->GetCurrentFilename(true)); + + hlsSegmentFrames = 0; + } + + avfw->WriteVideoFrame(&frame); + ++hlsSegmentFrames; + } + } else - nvr->WriteVideo(&frame); + { + if (forceKeyFrames) + nvr->WriteVideo(&frame, true, true); + else + nvr->WriteVideo(&frame); + } } - if (showprogress && QDateTime::currentDateTime() > statustime) + if (QDateTime::currentDateTime() > statustime) { - LOG(VB_GENERAL, LOG_INFO, - QString("Processed: %1 of %2 frames(%3 seconds)"). - arg((long)curFrameNum).arg((long)total_frame_count). - arg((long)(curFrameNum / video_frame_rate))); + if (showprogress) + { + LOG(VB_GENERAL, LOG_INFO, + QString("Processed: %1 of %2 frames(%3 seconds)"). + arg((long)curFrameNum).arg((long)total_frame_count). + arg((long)(curFrameNum / video_frame_rate))); + } + + if (hls && hls->CheckStop()) + { + hls->UpdateStatus(kHLSStatusStopping); + stopSignalled = true; + } + statustime = QDateTime::currentDateTime(); statustime = statustime.addSecs(5); } if (QDateTime::currentDateTime() > curtime) { - if (honorCutList && m_proginfo && + if (honorCutList && m_proginfo && !hls && m_proginfo->QueryMarkupFlag(MARK_UPDATED_CUT)) { LOG(VB_GENERAL, LOG_NOTICE, @@ -1231,6 +1745,8 @@ int Transcode::TranscodeFile(const QString &inputname, delete [] newFrame; if (player_ctx) delete player_ctx; + if (frameQueue) + frameQueue->stop(); return REENCODE_CUTLIST_CHANGE; } @@ -1245,6 +1761,8 @@ int Transcode::TranscodeFile(const QString &inputname, delete [] newFrame; if (player_ctx) delete player_ctx; + if (frameQueue) + frameQueue->stop(); return REENCODE_STOPPED; } @@ -1255,6 +1773,9 @@ int Transcode::TranscodeFile(const QString &inputname, int percentage = curFrameNum * 100 / total_frame_count; + if (hls) + hls->UpdatePercentComplete(percentage); + if (jobID >= 0) JobQueue::ChangeJobComment(jobID, QObject::tr("%1% Completed @ %2 fps.") @@ -1271,26 +1792,62 @@ int Transcode::TranscodeFile(const QString &inputname, curFrameNum++; frame.frameNumber = 1 + (curFrameNum << 1); + + player->DiscardVideoFrame(lastDecode); } sws_freeContext(scontext); if (! fifow) { - if (m_proginfo) + if (avfw) + avfw->CloseFile(); + + if (avfw2) + avfw2->CloseFile(); + + if (!hls && m_proginfo) { m_proginfo->ClearPositionMap(MARK_KEYFRAME); m_proginfo->ClearPositionMap(MARK_GOP_START); m_proginfo->ClearPositionMap(MARK_GOP_BYFRAME); } - nvr->WriteSeekTable(); - if (!kfa_table->empty()) - nvr->WriteKeyFrameAdjustTable(*kfa_table); + if (nvr) + { + nvr->WriteSeekTable(); + if (!kfa_table->empty()) + nvr->WriteKeyFrameAdjustTable(*kfa_table); + } } else { fifow->FIFODrain(); } + if (avfw) + delete avfw; + + if (avfw2) + delete avfw2; + + if (hls) + { + if (!stopSignalled) + { + hls->UpdateStatus(kHLSStatusCompleted); + hls->UpdateStatusMessage("Transcoding Completed"); + hls->UpdatePercentComplete(100); + } + else + { + hls->UpdateStatus(kHLSStatusStopped); + hls->UpdateStatusMessage("Transcoding Stopped"); + } + delete hls; + } + + if (frameQueue) + frameQueue->stop(); + delete [] newFrame; if (player_ctx) delete player_ctx; diff --git a/mythtv/programs/mythtranscode/transcode.h b/mythtv/programs/mythtranscode/transcode.h index 19e864954a2..c7ee63e823b 100644 --- a/mythtv/programs/mythtranscode/transcode.h +++ b/mythtv/programs/mythtranscode/transcode.h @@ -25,6 +25,18 @@ class Transcode : public QObject int AudioTrackNo, bool passthru = false); void ShowProgress(bool val) { showprogress = val; } void SetRecorderOptions(QString options) { recorderOptions = options; } + void SetAVFMode(void) { avfMode = true; } + void SetHLSMode(void) { hlsMode = true; } + void SetHLSStreamID(int streamid) { hlsStreamID = streamid; } + void SetHLSMaxSegments(int segments) { hlsMaxSegments = segments; } + void SetCMDContainer(QString container) { cmdContainer = container; } + void SetCMDAudioCodec(QString codec) { cmdAudioCodec = codec; } + void SetCMDVideoCodec(QString codec) { cmdVideoCodec = codec; } + void SetCMDHeight(int height) { cmdHeight = height; } + void SetCMDWidth(int width) { cmdWidth = width; } + void SetCMDBitrate(int bitrate) { cmdBitrate = bitrate; } + void SetCMDAudioBitrate(int bitrate) { cmdAudioBitrate = bitrate; } + void DisableAudioOnlyHLS(void) { hlsDisableAudioOnly = true; } private: bool GetProfile(QString profileName, QString encodingType, int height, @@ -44,6 +56,18 @@ class Transcode : public QObject KFATable *kfa_table; bool showprogress; QString recorderOptions; + bool avfMode; + bool hlsMode; + int hlsStreamID; + bool hlsDisableAudioOnly; + int hlsMaxSegments; + QString cmdContainer; + QString cmdAudioCodec; + QString cmdVideoCodec; + int cmdWidth; + int cmdHeight; + int cmdBitrate; + int cmdAudioBitrate; }; /* vim: set expandtab tabstop=4 shiftwidth=4: */