HTTP Live streaming and DASH support for MythTV.
Why:
- Although support for HTTP Live Streaming (
HLS) was added to MythTV in v0.25 it is not yet usable. - MythWeb is no longer actively developed and is based on Flash technology from the days of yore.
- HTML5 support for WebFrontend is work in progress.
- The new Web Application (MythTV v34) interface does not yet provide HTTP streaming support.
- An attempt to provide streaming support was made in github project MythTV stream mpeg DASH.
- All lack support for
HLS, Adaptive Bitrate Streaming (ABR), live recording, live broadcast, subtitles, etcetera.
What:
- Support for HTTP based streaming (serving) of HDHomeRun, MythTV and MythVideo content.
- Support for device independent viewing: web browser—mobile, desktop, tablet, etc.
- Support for less reliable networks via video and audio renditions (e.g. cell phone browser).
- Support for live tv, live recording, video and recorded content.
- Support for offline viewing.
- Support for at most one (text based) embedded subtitle stream or external subtitle file.
- Support for multiple audio languages (and audio renditions) in MythVideo content.
- Support for MythTV cutlist (commercial cut) created using Mythfrontend.
How:
- MPEG-DASH and HLS with fragmented MP4 (fMP4) makes both compatible, therefore only the manifest file (playlist) is different.
- Video is codified in H.264 format and audio in AAC.
- Encode MythTV recordings and MythVideo content with
FFmpegproviding playlist typeslive,eventandVOD. - Transcode to
MP4for offline playback on mobile devices. - HW accelerated support for VAAPI (other hw acceleration options are untested).
- Simple browser UI.
- Transcode videos to user defined (UI select dropdown list) renditions for adaptive playback.
- A Live TV UI allows for channel selection from a dropdown list.
- Optionally configure your preferred languages in the
phpfiles. - Optionally a ramdisk can be used for in memory handling of playlist
type
live. - Optionally shutdown lock can be used to prevent MythWelcome from shutting down.
The installation below is based on Fedora and Apache as web server. Adapting the code to your distribution / web server is left as an exercise to the user.
Note: do not run the code as is in an untrusted environment and do not open your web server to the internet. In either case additional security measures should be taken. Use at your own risk.
- MythTV
- Basically any version can be used. However, only version v0.34 can provide the full experience via the new Web Application UI.
- FFmpeg (for encoding)1
- FFmpeg version 5.1.3
- GNU screen
- This is to allow monitoring of encoding and to support background processes launched by the web-facing PHP script.
- version 4.9.0
- Shaka player
- This is used as the built-in Javascript-based browser player.
- version 4.3.6
- Mediainfo
- Used to loopup technical information about media files.
- version 23.09
- HDHomeRun
- Firmware Version 20230713
sudo dnf install mythtv ffmpeg screen mediainfo inotify-tools hdhomerun-devel sed mediainfo libva-utils intel-mediasdk mesa-va-driversCreate a web content owner2:
sudo useradd -d /var/www/ -g apache -M -N -s /sbin/nologin apache
sudo chown -R apache:apache /var/www/html
sudo chmod -R 755 /var/www/htmlThe backend code generates bash scripts. The commands in the scripts
should be run as a web content owner user, e.g. apache3, using
sudo.
cat /etc/sudoers.d/apache
apache ALL=(ALL) NOPASSWD: /usr/bin/hdhomerun_config, /usr/bin/ffmpeg, /usr/bin/realpath, /usr/bin/sed, /usr/bin/tail, /usr/bin/chmod, /usr/bin/mediainfo, /usr/bin/screen, /usr/bin/echo, /usr/bin/mkdir, /usr/bin/bash, /usr/bin/awkFill the content of a web content owner, e.g. apache,file as shown
above.
sudo visudo -f /etc/sudoers.d/apachegit clone https://github.com/shaka-project/shaka-player.git
cd shaka-player
python build/all.py
sudo mkdir -p /var/www/html/dist
sudo chown apache:apache /var/www/html/dist
sudo -uapache rsync -avh dist/ /var/www/html/dist/git clone https://github.com/alders/mythtv-stream-hls-dash.git
sudo mkdir -p /var/www/html/mythtv-stream-hls-dash
sudo chown apache:apache /var/www/html/mythtv-stream-hls-dash
sudo -uapache rsync -avnh --exclude='.git/' mythtv-stream-hls-dash/*.php /var/www/html/mythtv-stream-hls-dash/Optional step, modify 2 lines of
MythWeb code to change ASX Stream
button on the "Recorded Programs" page to Stream HLS DASH button.
Click me to configure MythWeb.
diff --git a/modules/tv/tmpl/default/recorded.php b/modules/tv/tmpl/default/recorded.php
index 8502305b..7bf3db0b 100644
--- a/modules/tv/tmpl/default/recorded.php
+++ b/modules/tv/tmpl/default/recorded.php
@@ -158,8 +158,8 @@ EOM;
echo ' -noimg">';
?>
<a class="x-download"
- href="<?php echo video_url($show, true) ?>" title="<?php echo t('ASX Stream'); ?>"
- ><img height="24" width="24" src="<?php echo skin_url ?>/img/play_sm.png" alt="<?php echo t('ASX Stream'); ?>"></a>
+ target="_blank" href="/mythtv-stream-hls-dash/index.php?filename=<?php echo $show->chanid."_".gmdate('YmdHis', $show->recstartts) ?>" title="<?php echo 'Stream HLS DASH'; ?>"
+ ><img height="24" width="24" src="<?php echo skin_url ?>/img/play_sm.png" alt="<?php echo 'Stream HLS DASH'; ?>"></a>
<a class="x-download"
href="<?php echo $show->url ?>" title="<?php echo t('Direct Download'); ?>"
><img height="24" width="24" src="<?php echo skin_url ?>/img/video_sm.png" alt="<?php echo t('Direct Download'); ?>"></a>Optionally change a few lines in the Web
Application4 to allow
recording and / or video and / or live tv selection from your browser.
Replace yourserver in the patches below to point to your combined web
server / mythbackend address.
Click me to configure web browser recording selection for playback on any device.
diff --git a/mythtv/html/backend/src/app/dashboard/recordings/recordings.component.html b/mythtv/html/backend/src/app/dashboard/recordings/recordings.component.html
index 4618e41aa8..8bae11e03a 100644
--- a/mythtv/html/backend/src/app/dashboard/recordings/recordings.component.html
+++ b/mythtv/html/backend/src/app/dashboard/recordings/recordings.component.html
@@ -76,7 +76,8 @@
<td style="flex-basis: 12%" class="p-1 overflow-hidden">
<i class="pi pi-exclamation-triangle p-1" *ngIf="program.VideoPropNames.indexOf('DAMAGED') > -1"
pTooltip="{{ 'dashboard.recordings.damaged' | translate }}" tooltipPosition="top"></i>
- {{program.Title}}
+ <a href="{{URLencode('http://yourserver/mythtv-stream-hls-dash/index.php?filename=' + program.Recording.FileName.split('.').slice(0, -1).join('.'))}}" target="_blank">{{program.Title}}</a></td>
+
</td>
<td style="flex-basis: 2%" class="p-1">
<i class="pi pi-eye" *ngIf="program.ProgramFlagNames.indexOf('WATCHED') > -1"Click me to configure web browser video selection for playback on any device.
diff --git a/mythtv/html/backend/src/app/dashboard/videos/videos.component.html b/mythtv/html/backend/src/app/dashboard/videos/videos.component.html
index 2d75b5e0ab..42abea28ac 100644
--- a/mythtv/html/backend/src/app/dashboard/videos/videos.component.html
+++ b/mythtv/html/backend/src/app/dashboard/videos/videos.component.html
@@ -68,7 +68,7 @@
(click)="onDirectory(video.Title)" label="{{video.Title}}"></button>
</div>
<ng-template #title>
- {{video.Title}}
+ <a href="{{URLencode('http://yourserver/mythtv-stream-hls-dash/index.php?videoid=' + video.Id)}}" target="_blank">{{video.Title}}</a>
</ng-template>
</td>
<td style="flex-basis: 3%" class="p-1">Click me to configure web browser tv channel selection for playback on any device.
diff --git a/mythtv/html/backend/src/app/guide/components/channelicon/channelicon.component.html b/mythtv/html/backend/src/app/guide/components/channelicon/channelicon.component.html
index 44abe96fea..c17429ef6c 100644
--- a/mythtv/html/backend/src/app/guide/components/channelicon/channelicon.component.html
+++ b/mythtv/html/backend/src/app/guide/components/channelicon/channelicon.component.html
@@ -4,6 +4,6 @@
<ng-template #nullIcon><img height="0" width="0"></ng-template>
</div>
<div class="channelText">
- <span>{{ channel.ChanNum}} {{ channel.CallSign }}</span>
+ <span><a href="{{URLencode('http://yourserver/mythtv-stream-hls-dash/hdhomerunstream.php?quality[]=high480&hw=h264&channel=' + CallSignEncode(channel.CallSign) + '&do=Watch+TV')}}" target="_blank">{{channel.ChanNum}} {{ channel.CallSign }}</a></span>
</div>
</div>
diff --git a/mythtv/html/backend/src/app/guide/components/channelicon/channelicon.component.ts b/mythtv/html/backend/src/app/guide/components/channelicon/channelicon.component.ts
index 97ae71efa8..f088012f94 100644
--- a/mythtv/html/backend/src/app/guide/components/channelicon/channelicon.component.ts
+++ b/mythtv/html/backend/src/app/guide/components/channelicon/channelicon.component.ts
@@ -16,4 +16,12 @@ export class ChannelIconComponent implements OnInit {
ngOnInit(): void {
}
+ URLencode(x: string): string {
+ let trimmed = x.replace(/\s+/g, '');
+ return encodeURI(trimmed);
+ }
+ CallSignEncode(x: string): string {
+ return x.replace(/\//g, '');
+ }
}Click me to configure web browser adding video search.
diff --git a/mythtv/html/backend/src/app/dashboard/videos/videos.component.html b/mythtv/html/backend/src/app/dashboard/videos/videos.component.html
index 2d75b5e0ab..5302e8724e 100644
--- a/mythtv/html/backend/src/app/dashboard/videos/videos.component.html
+++ b/mythtv/html/backend/src/app/dashboard/videos/videos.component.html
@@ -22,6 +22,7 @@
styleClass="p-button-primary">
</p-button>
</div>
+ <p-columnFilter type="text" field="Title" [matchModeOptions]="matchModeOptions"></p-columnFilter>
<p-checkbox inputId="showAllVideos" [(ngModel)]="showAllVideos" name="showAllVideos"
diff --git a/mythtv/html/backend/src/app/dashboard/videos/videos.component.ts b/mythtv/html/backend/src/app/dashboard/videos/videos.component.ts
index e46aa6c0aa..9c9a2a9aaa 100644
--- a/mythtv/html/backend/src/app/dashboard/videos/videos.component.ts
+++ b/mythtv/html/backend/src/app/dashboard/videos/videos.component.ts
@@ -5,6 +5,7 @@ import { LazyLoadEvent, MenuItem, MessageService } from 'primeng/api';
import { Menu } from 'primeng/menu';
import { Table } from 'primeng/table';
import { PartialObserver } from 'rxjs';
+import { SelectItem, FilterService, FilterMatchMode } from 'primeng/api';
import { GetVideoListRequest, UpdateVideoMetadataRequest, VideoMetadataInfo } from 'src/app/services/interfaces/video.interface';
import { UtilityService } from 'src/app/services/utility.service';
import { VideoService } from 'src/app/services/video.service';
@@ -22,6 +23,7 @@ export class VideosComponent implements OnInit {
@ViewChild("table") table!: Table;
videos: VideoMetadataInfo[] = [];
+ searchVideos: VideoMetadataInfo[] = [];
refreshing = false;
successCount = 0;
errorCount = 0;
@@ -33,6 +35,20 @@ export class VideosComponent implements OnInit {
showAllVideos = false;
lazyLoadEvent!: LazyLoadEvent;
+ matchModeOptions = [{
+ value: FilterMatchMode.STARTS_WITH,
+ label: 'Starts With',
+ },
+ {
+ value: FilterMatchMode.CONTAINS,
+ label: 'Contains',
+ },
+ {
+ value: FilterMatchMode.EQUALS,
+ label: 'Equals',
+ }
+ ];
+
mnu_markwatched: MenuItem = { label: 'dashboard.recordings.mnu_markwatched', command: (event) => this.markwatched(event, true) };
mnu_markunwatched: MenuItem = { label: 'dashboard.recordings.mnu_markunwatched', command: (event) => this.markwatched(event, false) };
mnu_updatemeta: MenuItem = { label: 'dashboard.recordings.mnu_updatemeta', command: (event) => this.updatemeta(event) };
@@ -73,7 +89,7 @@ export class VideosComponent implements OnInit {
let request: GetVideoListRequest = {
Sort: "title",
Folder: this.directory.join('/'),
- CollapseSubDirs: !this.showAllVideos,
+ CollapseSubDirs: this.showAllVideos,
StartIndex: 0,
Count: 1
};
@@ -91,17 +107,60 @@ export class VideosComponent implements OnInit {
request.Count = event.rows;
}
- this.videoService.GetVideoList(request).subscribe(data => {
- let newList = data.VideoMetadataInfoList;
- this.videos.length = data.VideoMetadataInfoList.TotalAvailable;
- // populate page of virtual programs
- this.videos.splice(newList.StartIndex, newList.Count,
- ...newList.VideoMetadataInfos);
- // notify of change
- this.videos = [...this.videos]
- this.refreshing = false;
- });
+ let regex: RegExp;
+ let regexDefined: boolean = false;
+ if (event.filters) {
+ if (event.filters.Title.value) {
+ switch (event.filters.Title.matchMode) {
+ case FilterMatchMode.STARTS_WITH:
+ regex = new RegExp('^' + event.filters.Title.value, "i");
+ regexDefined = true;
+ break;
+ case FilterMatchMode.CONTAINS:
+ regex = new RegExp(event.filters.Title.value, "i");
+ regexDefined = true;
+ break;
+ case FilterMatchMode.EQUALS:
+ regex = new RegExp('^' + event.filters.Title.value + '$', "i");
+ regexDefined = true;
+ break;
+ }
+ }
+ }
+ if (regexDefined) {
+ // This is not lazy...
+ let requestAll: GetVideoListRequest = {
+ Sort: "title",
+ Folder: this.directory.join('/'),
+ CollapseSubDirs: this.showAllVideos,
+ StartIndex: 0,
+ };
+
+ this.videoService.GetVideoList(requestAll).subscribe(data => {
+ let newList = data.VideoMetadataInfoList;
+ this.searchVideos.length = data.VideoMetadataInfoList.TotalAvailable;
+ this.searchVideos.splice(newList.StartIndex, newList.Count,
+ ...newList.VideoMetadataInfos);
+ this.searchVideos = this.searchVideos.filter((value) => regex.test(value.Title));
+ if (this.searchVideos.length > 0) {
+ // notify of change
+ this.videos = [...this.searchVideos];
+ this.refreshing = false;
+ }
+ });
+ } else {
+ this.videoService.GetVideoList(request).subscribe(data => {
+ let newList = data.VideoMetadataInfoList;
+ this.videos.length = data.VideoMetadataInfoList.TotalAvailable;
+ // populate page of virtual programs
+ this.videos.splice(newList.StartIndex, newList.Count,
+ ...newList.VideoMetadataInfos);
+ // notify of change
+ this.videos = [...this.videos]
+ this.refreshing = false;
+ });
+ }
}To apply these optional Web Application changes run the npm build script and install the web application.
Click me to run the npm build script.
cd mythtv/mythtv/html/backend/
npm run-script build
cd ..
sudo make installOptional step, add these (or similar) lines depending on your
installation to /etc/fstab to create a ramdisk for playlist live and
channel.
Click me to configure a ramdisk
tmpfs /var/www/html/live tmpfs nodev,nosuid,noexec,nodiratime,size=200M 0 0
tmpfs /var/www/html/channel tmpfs nodev,nosuid,nodiratime,size=200M 0 0Required configuration (check the php files):
- $webroot – This is the root of your web server.
- $webuser – This is the web content run user.
- $xml – Make sure your MythTV
Config.xml is readable by
$webuser.
Optional configuration (check the php files):
- $hlsdir – This is the directory where the meta data of all encoded
videos are stored. Moreover playlist
eventvideos are stored here. - $livedir – This is the directory where playlist
livevideos are stored. - $voddir – This is the directory where playlist
vodvideos are stored. - $ffmpeg – This variable points to the
FFmpegexecutable. Note, one may point this variable tomythffmpeg. However subtitles handling would not be supported. - $hwaccels – This array specifies the hw acceleration options for
FFmpeg. Note: onlyh264andnohwaccelhas been tested. - $settings – This array specifies the ladder the user may choose for his renditions.
- $sublangpref – This array contains your preferred languages in order. If available, the first match from top to bottom will be used as subtitle.
Allow JavaScript in your browser.
Figure 1 shows the user interface of mythtv-stream-hls-dash after
selecting a recording in MythWeb
or the new Web
Application5.
In case you do not want to patch MythWeb and the new Web Application find the filename in your recording directory, remove the extension from the filename and browse to http://yourserver/mythtv-stream-hls-dash/index.php?filename=NNNN_NNNNNNNNNNNNNN. For video extract the videoid from the download link in Web Application and browse to http://yourserver/mythtv-stream-hls-dash/video.php?videoid=NNNN.
Figure 1: User interface.
User interface options from top to bottom:
- Select an available recording from the list box6.
- Select the
ABRrenditions from the select dropdown list box. - Select the HW acceleration from the list box7.
- Select if the
Cutlistshould be used using the list box8. - Select using the checkbox if
Subtitlesshould be created9. - Select using the checkboxes if playlist type
liveoreventshould be used10. - Select using the checkbox if playlist type
VODshould be used. - Select using the checkbox if a
MP4file should be created. - Press Encode Video when you are satisfied with your choices to start encoding.
The selections shown in Figure 1 are used in the descriptions below as a running example.
Though not shown here, the user interface after selecting a video instead of a recording from Web Application has the same look and feel. Most functionality for a video and a recording is overlapping, but there are distinct differences as well. For example commercial cut is only available for recordings not for video. On the contrary multiple audio streams are supported for video not for recordings.
Figure 2 shows in more detail the user interface (phone interface) to select the renditions for Adaptive Bitrate Streaming (ABR). Use Ctrl-Click (Windows), Command-Click (Apple) to select the renditions.
Figure 2: Adaptive Bitrate UI.
This remux step is performed when the commercials are manually
cut in mythfrontend,
and the use of the Cutlist was selected in the UI. Remuxing may also
be required when otherwise the input video format cannot be processed
(e.g. avi). In the latter case this is done automagically.
Figure 3 shows the user interface while remuxing. Because
Cut Commercials was selected in Figure 1, the video is remuxed to an
MP4 container.
Figure 3: Remuxing UI.
Three buttons are shown below the available recording list box.
The first button Delete Video Files basically does what is says11.
The second status button displays a dynamic message. Figure 3 shows the
Remuxing Video percentage.
The third button Shutdown Lock can be used to prevent
MythWelcome from
shutting down by increasing the value larger than zero in the web page.
In combination with Wake-On-Lan (WOL) configured on your mythbackend
machine this allows one to have full control over MythTV from your
browser.
Figure 4 shows the user interface while encoding the video.
Figure 4: Generating m3u8.
Progress of the encoding is shown on the status button as a percentage and the time of the video available. When there is about 24 seconds of content available the player automatically tries to load the video12.
At the right hand side of the Shutdown Lock button additional buttons
dynamically appear when files become available on disk. In Figure 4 this
is the case for HLS event, HLS VOD, and DASH VOD. The video should
load automagically within 30 seconds. If this does not happen, select
either of the buttons to start playing. As a last resort one could
reload the web page.
Old devices not supporting the Shaka video player of the UI, may still be able to play media through the buttons provided. The buttons link to the various manifest files.
The Shaka-player UI supports Chromecast out of the box, provided your
website is running HTTPS. Having self signed certificates on your
private intranet is unfortunately not enough. Alternatively, the http
links can also be copied and used in your favorite app. Chromecast of
http streams is e.g. supported by
vlc.
Figure 5 shows what happens in case the status button is selected. This will trigger a popup message box with a detailed view of the steps involved and the status thereof.
In this example three processing steps were required:
- Remux to
mp4container for commercial cut. - Encoding to the various playlists.
- Subtitle merge into the
mp4file.
Figure 5: Status UI.
Figure 6 shows the interface after encoding is done.
Figure 6: User interface after encoding.
Two additional buttons appeared in Figure 6, Cleanup Video Files 13
and Download MP4.
Since both playlists HLS event and HLS VOD basically provide similar
user experience for HLS one may decide to remove the playlist
HLS event files to reduce disk space. This is exactly what the
Cleanup Video Files button does.
The UI also shows a Download MP4 link as was requested in Figure 1.
The latter is only visible when the encoding has finished and optionally
subtitles are mixed in.
After pressing the Encode Video in Figure 1 a bash shell script is
generated. For illustration purposes the code for the running example is
shown in separate code blocks below.
The user in Figure 1 selected Cut Commercials. This requires the input
video to be remuxed to a MP4 container as shown in the user interface
of Figure 3. The code block below shows in detail how this is done.
An MP4 container allows FFmpeg to use the concat demuxer later in
the script14.
Click me
cd /var/www/html/hls/10100_20231101212100
/usr/bin/sudo /usr/bin/screen -S 10100_20231101212100_remux -dm /usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: remux start > /var/www/html/hls/10100_20231101212100/status.txt;
/usr/bin/sudo -uapache /usr/bin/ffmpeg \
-y \
-hwaccel vaapi -vaapi_device /dev/dri/renderD128 \ # Use VAAPI Hardware acceleration
-txt_format text -txt_page 888 \ # extract subtitles from dvb_teletext
-fix_sub_duration \ # avoid overlap of subtitles
-i /mnt/mythtv2/store//10100_20231101212100.ts \ # input file recorded with HDHomeRun
-c copy \ # use encoder copy for video and audio
-c:s mov_text \ # set subtitle codec to mov_text
/var/www/html/hls/10100_20231101212100/video.mp4 && \
/usr/bin/echo `date`: remux finish success >> /var/www/html/hls/10100_20231101212100/status.txt || \
/usr/bin/echo `date`: remux finish failed >> /var/www/html/hls/10100_20231101212100/status.txt'
while [ ! "`/usr/bin/cat /var/www/html/hls/10100_20231101212100/status.txt | /usr/bin/grep 'remux finish success'`" ] ; \
do \
sleep 1; \
done
(while [ ! -f "/var/www/html/hls/10100_20231101212100/master_event.m3u8" ] ;
do
/usr/bin/inotifywait -e close_write --include "master_event.m3u8" /var/www/html/hls/10100_20231101212100;
done;Adapt the playlist master_event.m3u8 as soon as the file is created by
FFmpeg some time in the future. This allows the player to start at the
beginning of the video and if present defines the handling of the
subtitle.
Click me
(while [ ! -f "/var/www/html/hls/10100_20231101212100/master_event.m3u8" ] ;
do
/usr/bin/inotifywait -e close_write --include "master_event.m3u8" /var/www/html/hls/10100_20231101212100;
done;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-VERSION:7)/\1\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles",NAME="Dutch",DEFAULT=YES,FORCED=NO,AUTOSELECT=YES,URI="sub_0_vtt.m3u8",LANGUAGE="dut"/' /var/www/html/hls/10100_20231101212100/master_event.m3u8;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-VERSION:7)/\1\n#EXT-X-START:TIME-OFFSET=0/' /var/www/html/hls/10100_20231101212100/master_event.m3u8;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-STREAM.*)/\1,SUBTITLES="subtitles"/' /var/www/html/hls/10100_20231101212100/master_event.m3u8; /usr/bin/sudo -uapache /usr/bin/sudo sed -r '/(#EXT-X-STREAM-INF:BANDWIDTH=[0-9]+\,CODECS)/{N;d;}' -i /var/www/html/hls/10100_20231101212100/master_event.m3u8;) &Adapt the playlist master_vod.m3u8 file as soon as the file is created
by FFmpeg some time in the future. This allows the player to start at
the beginning of the video. Additionally if present the language of the
subtitle and the languages(s) of the audio is defined.
Click me
(while [ ! -f "/var/www/html/vod/10100_20231101212100/master_vod.m3u8" ] ;
do
/usr/bin/inotifywait -e close_write --include "master_vod.m3u8" /var/www/html/vod/10100_20231101212100;
done;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-VERSION:7)/\1\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles",NAME="Dutch",DEFAULT=YES,FORCED=NO,AUTOSELECT=YES,URI="sub_0_vtt.m3u8",LANGUAGE="dut"/' /var/www/html/vod/10100_20231101212100/master_vod.m3u8;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-VERSION:7)/\1\n#EXT-X-START:TIME-OFFSET=0/' /var/www/html/vod/10100_20231101212100/master_vod.m3u8;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-STREAM.*)/\1,SUBTITLES="subtitles"/' /var/www/html/vod/10100_20231101212100/master_vod.m3u8;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="group_A1")/\1,LANGUAGE="dut"/' /var/www/html/vod/10100_20231101212100/master_vod.m3u8;) &The major part of the encoding is done in one FFmpeg command utilizing
filter_complex and tee to the max. This code block starts the actual
encoding and waits until it is finished.
Click me
/usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: encode start >> /var/www/html/hls/10100_20231101212100/status.txt';
/usr/bin/sudo -uapache /usr/bin/mkdir -p /var/www/html/vod/10100_20231101212100;
/usr/bin/sudo -uapache /usr/bin/mkdir -p /var/www/html/hls/10100_20231101212100;
cd /var/www/html/hls/;
/usr/bin/sudo -uapache /usr/bin/ffmpeg \
-fix_sub_duration \
-txt_format text -txt_page 888 \
-hwaccel vaapi -vaapi_device /dev/dri/renderD128 \
\
\
-f concat -async 1 -safe 0 -i /var/www/html/hls/10100_20231101212100/cutlist.txt \ # Use cutlist
-progress 10100_20231101212100/progress-log.txt \ # Track progress of encoding
-live_start_index 0 \ # Segment index to start live streams at
-force_key_frames "expr:gte(t,n_forced*2)" \ # Fixed key frame interval is needed to avoid variable segment duration.
-tune film \ # use for high quality movie content; lowers deblocking
-metadata title="De Avondshow met Arjen Lubach" \
-force_key_frames "expr:gte(t,n_forced*2)" \
-filter_complex "[0:v]split=3[v1][v2][v3];[v1]format=nv12|vaapi,hwupload,scale_vaapi=w=1280:h=720[v1out];[v2]format=nv12|vaapi,hwupload,scale_vaapi=w=854:h=480[v2out];[v3]format=nv12|vaapi,hwupload,scale_vaapi=w=640:h=360[v3out]" \
-map [v1out] -c:v:0 \ # Rendition 1
h264_vaapi \ # Use H264 VAAPI (Video Acceleration API) hardware acceleration
-b:v:0 3200k \ # Transcode Video 1 to a user selected bitrate
-maxrate:v:0 3200k \ # Maximum bitrate
-bufsize:v:0 1.5*3200k \ # Buffer size
-crf 23 \ # Constant Rate Factor
-preset veryslow \ #
-g 48 \ #
-keyint_min 48 \ # Set minimum interval between IDR-frame
-sc_threshold 0 \ # Sets the threshold for the scene change detection.
-flags +global_header \ # Set global header in the bitstream.
-map [v2out] -c:v:1 \ # Rendition 2
h264_vaapi \ # Use H264 VAAPI (Video Acceleration API) hardware acceleration
-b:v:1 1600k \ # Transcode Video 2 to a derived lower resolution based on a user selected bitrate
-maxrate:v:1 1600k \ # Maximum bitrate
-bufsize:v:1 1.5*1600k \ # Buffer size
-crf 23 \ # Constant Rate Factor
-preset veryslow \ #
-g 48 \ #
-keyint_min 48 \ # Set minimum interval between IDR-frame
-sc_threshold 0 \ # Sets the threshold for the scene change detection.
-flags +global_header \ # Set global header in the bitstream.
-map [v3out] -c:v:2 \ # Rendition 3
h264_vaapi \ # Use H264 VAAPI (Video Acceleration API) hardware acceleration
-b:v:2 900k \ # Transcode Video 3 to a derived lower resolution based on a user selected bitrate
-maxrate:v:2 900k \ # Maximum bitrate
-bufsize:v:2 1.5*900k \ # Buffer size
-crf 23 \ # Constant Rate Factor
-preset veryslow \ #
-g 48 \ #
-keyint_min 48 \ # Set minimum interval between IDR-frame
-sc_threshold 0 \ # Sets the threshold for the scene change detection.
-flags +global_header \ # Set global header in the bitstream.
\
-map a:0 -c:a:0 aac -b:a:0 128k -ac 2 \
-metadata:s:a:0 language=dut \
\
-map 0:s:0 -c:s webvtt -metadata:s:s:0 language=dut \
-f tee \
"[select=\'a:0,v:0,v:1,v:2\': \
f=dash: \
seg_duration=2: \
hls_playlist=true: \
single_file=true: \
adaptation_sets=\'id=0,streams=a id=1,streams=v\' : \
media_seg_name=\'stream_vod_$RepresentationID$-$Number%05d$.$ext$\': \
hls_master_name=master_vod.m3u8]../vod/10100_20231101212100/manifest_vod.mpd| \
[select=\'v:0,s:0\': \
strftime=1: \
hls_flags=+independent_segments+iframes_only: \
hls_time=2: \
hls_playlist_type=event: \
hls_segment_type=fmp4: \
var_stream_map=\'v:0,s:0,sgroup:subtitle\': \
hls_segment_filename=\'/dev/null\']../vod/10100_20231101212100/sub_%v.m3u8| \
[select=\'v:0,a:0\': \
f=mp4: \
movflags=+faststart]10100_20231101212100/10100_20231101212100 - De Avondshow met Arjen Lubach.mp4| \
[select=\'s:0\']10100_20231101212100/subtitles.vtt| \
/dev/null| \
[select=\'a:0,v:0,v:1,v:2\': \
f=hls: \
hls_time=2: \
hls_playlist_type=event: \
hls_flags=+independent_segments+iframes_only: \
hls_segment_type=fmp4: \
var_stream_map=\'a:0,agroup:aac,language:dut,name:aac_0_128k v:0,agroup:aac,name:720p_3200 v:1,agroup:aac,name:480p_1600 v:2,agroup:aac,name:360p_900\': \
master_pl_name=master_event.m3u8: \
hls_segment_filename=10100_20231101212100/stream_event_%v_data%02d.m4s]10100_20231101212100/stream_event_%v.m3u8| \
[select=\'v:0,s:0\': \
strftime=1: \
f=hls: \
hls_flags=+independent_segments+program_date_time: \
hls_time=2: \
hls_playlist_type=event: \
hls_segment_type=fmp4: \
var_stream_map=\'v:0,s:0,sgroup:subtitle\': \
hls_segment_filename=\'/dev/null\']10100_20231101212100/sub_%v.m3u8" \
2>>/tmp/ffmpeg-hls-10100_20231101212100.log && \
/usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: encode finish success >> /var/www/html/hls/10100_20231101212100/status.txt' || \
/usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: encode finish failed >> /var/www/html/hls/10100_20231101212100/status.txt'
while [ ! "`/usr/bin/cat /var/www/html/hls/10100_20231101212100/status.txt | /usr/bin/grep 'encode finish success'`" ] ;
do
sleep 1;
doneIn a post-processing step subtitles are added to the MP4.
Click me
cd /var/www/html/hls/10100_20231101212100;
/usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: subtitle_merge start >> /var/www/html/hls/10100_20231101212100/status.txt';
cd /var/www/html/hls/10100_20231101212100;
/usr/bin/sudo -uapache /usr/bin/ffmpeg \
-i "10100_20231101212100 - De Avondshow met Arjen Lubach.mp4" \
-i subtitles.vtt \
-c:s mov_text -metadata:s:s:0 language=dut -disposition:s:0 default \
-c:v copy \
-c:a copy \
"10100_20231101212100 - De Avondshow met Arjen Lubach.tmp.mp4" \
2>>/tmp/ffmpeg-subtitle-merge-hls-10100_20231101212100.log && \
/usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: subtitle_merge success >> /var/www/html/hls/10100_20231101212100/status.txt' || \
/usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: subtitle_merge failed >> /var/www/html/hls/10100_20231101212100/status.txt';
/usr/bin/sudo /usr/bin/mv -f "10100_20231101212100 - De Avondshow met Arjen Lubach.tmp.mp4" "10100_20231101212100 - De Avondshow met Arjen Lubach.mp4"
while [ ! "`/usr/bin/cat /var/www/html/hls/10100_20231101212100/status.txt | /usr/bin/grep 'encode finish success'`" ] ;
do
sleep 1;
done
/usr/bin/sudo /usr/bin/rm /var/www/html/hls/10100_20231101212100/video.mp4
sleep 3 && /usr/bin/sudo /usr/bin/screen -ls 10100_20231101212100_encode | /usr/bin/grep -E '\s+[0-9]+.' | /usr/bin/awk '{print $1}' - | while read s; do /usr/bin/sudo /usr/bin/screen -XS $s quit; doneFor completeness the whole script.
Click me
cd /var/www/html/hls/10100_20231101212100
/usr/bin/sudo /usr/bin/screen -S 10100_20231101212100_remux -dm /usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: remux start > /var/www/html/hls/10100_20231101212100/status.txt;
/usr/bin/sudo -uapache /usr/bin/ffmpeg \
-y \
-hwaccel vaapi -vaapi_device /dev/dri/renderD128 \
-txt_format text -txt_page 888 \
-fix_sub_duration \
-i "/mnt/mythtv2/store//10100_20231101212100.ts" \
-c copy \
-c:s mov_text \
/var/www/html/hls/10100_20231101212100/video.mp4 && \
/usr/bin/echo `date`: remux finish success >> /var/www/html/hls/10100_20231101212100/status.txt || \
/usr/bin/echo `date`: remux finish failed >> /var/www/html/hls/10100_20231101212100/status.txt'
while [ ! "`/usr/bin/cat /var/www/html/hls/10100_20231101212100/status.txt | /usr/bin/grep 'remux finish success'`" ] ; \
do \
sleep 1; \
done
(while [ ! -f "/var/www/html/hls/10100_20231101212100/master_event.m3u8" ] ;
do
/usr/bin/inotifywait -e close_write --include "master_event.m3u8" /var/www/html/hls/10100_20231101212100;
done;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-VERSION:7)/\1\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles",NAME="Dutch",DEFAULT=YES,FORCED=NO,AUTOSELECT=YES,URI="sub_0_vtt.m3u8",LANGUAGE="dut"/' /var/www/html/hls/10100_20231101212100/master_event.m3u8;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-VERSION:7)/\1\n#EXT-X-START:TIME-OFFSET=0/' /var/www/html/hls/10100_20231101212100/master_event.m3u8;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-STREAM.*)/\1,SUBTITLES="subtitles"/' /var/www/html/hls/10100_20231101212100/master_event.m3u8; /usr/bin/sudo -uapache /usr/bin/sudo sed -r '/(#EXT-X-STREAM-INF:BANDWIDTH=[0-9]+\,CODECS)/{N;d;}' -i /var/www/html/hls/10100_20231101212100/master_event.m3u8;) &
(while [ ! -f "/var/www/html/vod/10100_20231101212100/master_vod.m3u8" ] ;
do
/usr/bin/inotifywait -e close_write --include "master_vod.m3u8" /var/www/html/vod/10100_20231101212100;
done;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-VERSION:7)/\1\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles",NAME="Dutch",DEFAULT=YES,FORCED=NO,AUTOSELECT=YES,URI="sub_0_vtt.m3u8",LANGUAGE="dut"/' /var/www/html/vod/10100_20231101212100/master_vod.m3u8;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-VERSION:7)/\1\n#EXT-X-START:TIME-OFFSET=0/' /var/www/html/vod/10100_20231101212100/master_vod.m3u8;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-STREAM.*)/\1,SUBTITLES="subtitles"/' /var/www/html/vod/10100_20231101212100/master_vod.m3u8;
/usr/bin/sudo -uapache /usr/bin/sed -i -E 's/(#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="group_A1")/\1,LANGUAGE="dut"/' /var/www/html/vod/10100_20231101212100/master_vod.m3u8;) &
/usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: encode start >> /var/www/html/hls/10100_20231101212100/status.txt';
/usr/bin/sudo -uapache /usr/bin/mkdir -p /var/www/html/vod/10100_20231101212100;
/usr/bin/sudo -uapache /usr/bin/mkdir -p /var/www/html/hls/10100_20231101212100;
cd /var/www/html/hls/;
/usr/bin/sudo -uapache /usr/bin/ffmpeg \
-fix_sub_duration \
-txt_format text -txt_page 888 \
-hwaccel vaapi -vaapi_device /dev/dri/renderD128 \
\
\
-f concat -async 1 -safe 0 -i /var/www/html/hls/10100_20231101212100/cutlist.txt \ # Use cutlist
-progress 10100_20231101212100/progress-log.txt \ # Track progress of encoding
-live_start_index 0 \ # Segment index to start live streams at
-force_key_frames "expr:gte(t,n_forced*2)" \ # Fixed key frame interval is needed to avoid variable segment duration.
-tune film \ # use for high quality movie content; lowers deblocking
-metadata title="De Avondshow met Arjen Lubach" \
-force_key_frames "expr:gte(t,n_forced*2)" \
-filter_complex "[0:v]split=3[v1][v2][v3];[v1]format=nv12|vaapi,hwupload,scale_vaapi=w=1280:h=720[v1out];[v2]format=nv12|vaapi,hwupload,scale_vaapi=w=854:h=480[v2out];[v3]format=nv12|vaapi,hwupload,scale_vaapi=w=640:h=360[v3out]" \
-map [v1out] -c:v:0 \ # Rendition 1
h264_vaapi \ # Use H264 VAAPI (Video Acceleration API) hardware acceleration
-b:v:0 3200k \ # Transcode Video 1 to a user selected bitrate
-maxrate:v:0 3200k \ # Maximum bitrate
-bufsize:v:0 1.5*3200k \ # Buffer size
-crf 23 \ # Constant Rate Factor
-preset veryslow \ #
-g 48 \ #
-keyint_min 48 \ # Set minimum interval between IDR-frame
-sc_threshold 0 \ # Sets the threshold for the scene change detection.
-flags +global_header \ # Set global header in the bitstream.
-map [v2out] -c:v:1 \ # Rendition 2
h264_vaapi \ # Use H264 VAAPI (Video Acceleration API) hardware acceleration
-b:v:1 1600k \ # Transcode Video 2 to a derived lower resolution based on a user selected bitrate
-maxrate:v:1 1600k \ # Maximum bitrate
-bufsize:v:1 1.5*1600k \ # Buffer size
-crf 23 \ # Constant Rate Factor
-preset veryslow \ #
-g 48 \ #
-keyint_min 48 \ # Set minimum interval between IDR-frame
-sc_threshold 0 \ # Sets the threshold for the scene change detection.
-flags +global_header \ # Set global header in the bitstream.
-map [v3out] -c:v:2 \ # Rendition 3
h264_vaapi \ # Use H264 VAAPI (Video Acceleration API) hardware acceleration
-b:v:2 900k \ # Transcode Video 3 to a derived lower resolution based on a user selected bitrate
-maxrate:v:2 900k \ # Maximum bitrate
-bufsize:v:2 1.5*900k \ # Buffer size
-crf 23 \ # Constant Rate Factor
-preset veryslow \ #
-g 48 \ #
-keyint_min 48 \ # Set minimum interval between IDR-frame
-sc_threshold 0 \ # Sets the threshold for the scene change detection.
-flags +global_header \ # Set global header in the bitstream.
\
-map a:0 -c:a:0 aac -b:a:0 128k -ac 2 \
-metadata:s:a:0 language=dut \
\
-map 0:s:0 -c:s webvtt -metadata:s:s:0 language=dut \
-f tee \
"[select=\'a:0,v:0,v:1,v:2\': \
f=dash: \
seg_duration=2: \
hls_playlist=true: \
single_file=true: \
adaptation_sets=\'id=0,streams=a id=1,streams=v\' : \
media_seg_name=\'stream_vod_$RepresentationID$-$Number%05d$.$ext$\': \
hls_master_name=master_vod.m3u8]../vod/10100_20231101212100/manifest_vod.mpd| \
[select=\'v:0,s:0\': \
strftime=1: \
hls_flags=+independent_segments+iframes_only: \
hls_time=2: \
hls_playlist_type=event: \
hls_segment_type=fmp4: \
var_stream_map=\'v:0,s:0,sgroup:subtitle\': \
hls_segment_filename=\'/dev/null\']../vod/10100_20231101212100/sub_%v.m3u8| \
[select=\'v:0,a:0\': \
f=mp4: \
movflags=+faststart]10100_20231101212100/10100_20231101212100 - De Avondshow met Arjen Lubach.mp4| \
[select=\'s:0\']10100_20231101212100/subtitles.vtt| \
/dev/null| \
[select=\'a:0,v:0,v:1,v:2\': \
f=hls: \
hls_time=2: \
hls_playlist_type=event: \
hls_flags=+independent_segments+iframes_only: \
hls_segment_type=fmp4: \
var_stream_map=\'a:0,agroup:aac,language:dut,name:aac_0_128k v:0,agroup:aac,name:720p_3200 v:1,agroup:aac,name:480p_1600 v:2,agroup:aac,name:360p_900\': \
master_pl_name=master_event.m3u8: \
hls_segment_filename=10100_20231101212100/stream_event_%v_data%02d.m4s]10100_20231101212100/stream_event_%v.m3u8| \
[select=\'v:0,s:0\': \
strftime=1: \
f=hls: \
hls_flags=+independent_segments+program_date_time: \
hls_time=2: \
hls_playlist_type=event: \
hls_segment_type=fmp4: \
var_stream_map=\'v:0,s:0,sgroup:subtitle\': \
hls_segment_filename=\'/dev/null\']10100_20231101212100/sub_%v.m3u8" \
2>>/tmp/ffmpeg-hls-10100_20231101212100.log && \
/usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: encode finish success >> /var/www/html/hls/10100_20231101212100/status.txt' || \
/usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: encode finish failed >> /var/www/html/hls/10100_20231101212100/status.txt'
while [ ! "`/usr/bin/cat /var/www/html/hls/10100_20231101212100/status.txt | /usr/bin/grep 'encode finish success'`" ] ;
do
sleep 1;
done
cd /var/www/html/hls/10100_20231101212100;
/usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: subtitle_merge start >> /var/www/html/hls/10100_20231101212100/status.txt';
cd /var/www/html/hls/10100_20231101212100;
/usr/bin/sudo -uapache /usr/bin/ffmpeg \
-i "10100_20231101212100 - De Avondshow met Arjen Lubach.mp4" \
-i subtitles.vtt \
-c:s mov_text -metadata:s:s:0 language=dut -disposition:s:0 default \
-c:v copy \
-c:a copy \
"10100_20231101212100 - De Avondshow met Arjen Lubach.tmp.mp4" \
2>>/tmp/ffmpeg-subtitle-merge-hls-10100_20231101212100.log && \
/usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: subtitle_merge success >> /var/www/html/hls/10100_20231101212100/status.txt' || \
/usr/bin/sudo -uapache /usr/bin/bash -c '/usr/bin/echo `date`: subtitle_merge failed >> /var/www/html/hls/10100_20231101212100/status.txt';
/usr/bin/sudo /usr/bin/mv -f "10100_20231101212100 - De Avondshow met Arjen Lubach.tmp.mp4" "10100_20231101212100 - De Avondshow met Arjen Lubach.mp4"
while [ ! "`/usr/bin/cat /var/www/html/hls/10100_20231101212100/status.txt | /usr/bin/grep 'encode finish success'`" ] ;
do
sleep 1;
done
/usr/bin/sudo /usr/bin/rm /var/www/html/hls/10100_20231101212100/video.mp4
sleep 3 && /usr/bin/sudo /usr/bin/screen -ls 10100_20231101212100_encode | /usr/bin/grep -E '\s+[0-9]+.' | /usr/bin/awk '{print $1}' - | while read s; do /usr/bin/sudo /usr/bin/screen -XS $s quit; done- The current project code needs to be refactored in order to remove duplicate code.
- DVD menus are not supported.
- At most one embedded subtitle is supported.
- A design choice has been made to symlink
mp4files rather than to encode them.
User Jobs are customized tasks
which can act on MythTV recordings. Making use of the
mythtv-stream-hls-dash web server one can export recordings in mp4
compatible format for offline viewing.
Replace yourserver in the example script below to point to your
combined web server / mythbackend address. Copy the script to
/usr/local/bin/mythtv-stream-hls-dash.sh. Add a User Job in the
mythtv-setup program
/usr/local/bin/mythtv-stream-hls-dash.sh "%FILE%" or via
MythWeb settings, and restart
mythbackend.
Click me
#!/bin/bash
# Run this script with 1 argument:
#
# mythtv-stream-hls-dash.sh <filename>
#
FILENAME=`/usr/bin/basename $1`
FILENAME="${FILENAME%.*}"
curl -sS --data-urlencode "filename=$FILENAME" http://yourserver/mythtv-stream-hls-dash/index.php >/dev/null
curl -sS --data-urlencode "filename=$FILENAME" \
--data-urlencode "quality[]=high480" \
--data-urlencode "hw=h264" \
--data-urlencode "removecut=off" \
--data-urlencode "checkbox_subtitles=yes" \
--data-urlencode "clippedlength=0" \
--data-urlencode "cutcount=0" \
--data-urlencode "subtitles=on" \
--data-urlencode "mp4=on" \
--data-urlencode "do=Encode+Video" \
http://yourserver/mythtv-stream-hls-dash/index.php 1>/dev/null 2>/tmp/mythtv-stream-hls-dash-$FILENAME.txt
while [ ! "`/usr/bin/cat /var/www/html/hls/${FILENAME}/status.txt | /usr/bin/grep 'encode finish success'`" ] ;
do
if [[ "`/usr/bin/cat /var/www/html/hls/${FILENAME}/status.txt | /usr/bin/grep 'encode finish failed'`" ]]; then exit 1; fi
sleep 1;
done
exit 0A slightly more interesting User Job would be to add --data-urlencode "hls_playlist_type[]=event" and / or --data-urlencode "vod=on" to the second curl command, which would create playlist type
event and / or vod on the fly as well.
It would be even more interesting to add such a User Job at the start of
a recording, creating playlist streams while recording. Unfortunately,
these kind of User Jobs are not possible with MythTV. If this kind of
real time streaming is required one has to initiate this manually using
the mythtv-stream-hls-dash UI when the recording has started.
Playlist type (and MP4) support for live broadcast, video and recorded
video are shown in table 1. DASH is only supported by VOD, whereas
HLS (and ABR) is supported by all playlist types. Subtitles are
supported by all.
Table 1: Playlist and MP4 support for live broadcast and recorded video.
| Playlist | HLS | DASH | subtitle15 | subtitle16 | ABR |
|---|---|---|---|---|---|
| live | ✅ | ✅ | ✅ | ||
| event | ✅ | ✅ | ✅ | ||
| VOD | ✅ | ✅ | ✅ | ✅ | |
| MP4 | ✅ |
All possible UI combinations of playlist types and MP4 that can be chosen by the user are shown in table 217.
Table 2: All possible UI combinations of playlist types and MP4.
| live | event | VOD | MP4 |
|---|---|---|---|
| ✅ | |||
| ✅ | ✅ | ||
| ✅ | ✅ | ||
| ✅ | ✅ | ✅ | |
| ✅ | |||
| ✅ | ✅ | ||
| ✅ | ✅ | ||
| ✅ | ✅ | ✅ | |
| ✅ | |||
| ✅ | |||
| ✅ | ✅ |
Table 3, 4 and 5 shows feature support of the Safari built-in m3u8
player on macOS and Shaka player while encoding a set of random
renditions: 720p high, 480p normal, 360p low, and 240p low. As
is shown feature support varies. None of them provides the desired
combination i.e. allowing one to manually select the desired video
rendition and audio rendition18. Hopefully the players really do
provide the best possible bitrate for the network "automagically".
Table 3: Safari m3u8 player UI playlist support during Live Broadcasting (while encoding).
| Playlist | Progress bar | Subtitles | Resolution | Language |
|---|---|---|---|---|
| live | 🔴 | Dutch | 🔴 | (Dutch (audio_0)),..,Dutch (audio_2) |
| event | 🔴 | Dutch | 🔴 | (Dutch (audio_0)),..,Dutch (audio_2) |
| VOD | 🔴 | Dutch | 🔴 | (Dutch (audio_4)),..,Dutch (audio_6) |
| MP4 | ✅ | 🔴 | 🔴 |
Table 4: Shaka player (configuration ("useNativeHlsOnSafari" : true)) UI playlist support during Live Broadcasting (while encoding).
| Playlist | Progress bar | Captions | Resolution | Language | Quality |
|---|---|---|---|---|---|
| live | ✅ 19 | Nederlands | Auto (nullp) | Nederlands | 🔴 |
| event | ✅ | Nederlands | Auto (nullp) | Nederlands | 🔴 |
| VOD | ✅ | Nederlands | Auto (nullp) | Nederlands,Nederlands (2 out of 3 tracks) | 🔴 |
| MP4 | ✅ | 🔴 | Nederlands | Auto (0 kbits/s) |
Table 5: Safari Player (configuration ("useNativeHlsOnSafari" : false)) UI playlist support during Live Broadcasting (while encoding).
| Playlist | Progress bar | Captions | Resolution | Language | Quality |
|---|---|---|---|---|---|
| live | 🔴 | ✅ (off) | 240p | 🔴 | 🔴 |
| event | ✅ | ✅ (off) | 720p,.., 240p | Nederlands | 🔴 |
| VOD | 🔴 | ✅ (off) | 720p,.., 240p | Nederlands | 🔴 |
| MP4 | ✅ | 🔴 | Nederlands | Auto (0 kbits/s) |
Figure 7 shows the user interface of hdhomerunstream while selecting a
TV channel.
Figure 7: Select TV channel.
User interface options:
- Select the
ABRrenditions from the select dropdown list box, see Figure 2. - Select the HW acceleration from the list box20.
- Select the TV channel from the list box21.
- Press Watch TV when you are satisfied with your choices to start watching.
Figure 8 shows the Live TV user interface.
Figure 8: Live TV user interface.
User interface options:
- Select Stop streaming when you are done watching22. This also cleans up the files on disk.
- The status button indicates when the
Live stream is ready. - Select
Shutdown Lockin case one wants to prevent MythTV from shutting down. - The dynamic button at the right hand side indicates the fact that the
HLSmanifest file (no DASH support yet) is generated by showing the selected channel name.
- The HDHomeRun tuner is hardcoded. The tuner is basically assumed to be reserved no checks are implemented.
- Multiple devices can view the same channel. However, no checks are implemented when one of them stops the stream.
- Only
HLSis supported.
I would like to thank the MythTV stream mpeg DASH project for giving me the inspiration!
Thank you MythTV Devs, you have a top notch app and please continue all of your hard work, believe me it's much appreciated.
MythTV-stream-hls-dash is licensed under the GPLv3, see LICENSE for details.
Feedback, patches, other contributions and ideas are welcome!
Footnotes
-
mythffmpegcan be used instead, but does not support subtitles. ↩ -
May depend on your distribution (e.g. 'data-www' is used for Ubuntu). May require one to configure the
phpscripts. ↩ -
May depend on your distribution (e.g. 'data-www' is used for Ubuntu). May require one to configure the
phpscripts. ↩ -
This requires installation of mythtv v34 from sources. ↩
-
A subset of the user interface is used after selecting a video in Web Application. ↩
-
The dropdown list shows the recordings available for streaming. Leave as is since we are defining the settings for this recording. ↩
-
Only VAAPI and no HW acceleration has been tested. Feedback on untested acceleration is appreciated. ↩
-
This option is only visible in the UI when a
Cutlistis defined in MythTV. By default theCutlistis not selected (checked). ↩ -
This option is only visible and checked, by default, when subtitles are available. Internal embedded subtitles are handled based on the user defined language preferences in the php files. Alternatively, one external subtitle (.srt) is supported (no language preference check can be performed) which should have the same filename as the video. ↩
-
Either one of the two or none at all. ↩
-
This will not delete any file from MythTV or change the MySQL database. All files can be recreated as long as the recording is available in MythTV. ↩
-
If no still of the output is shown after 30 seconds, push the
HLS eventofHLS VODbutton. As a last resort try to reload the browser page. ↩ -
This button is only shown when both playlist types
eventandVODwere selected as shown in Figure 1. ↩ -
The
cutlistitself was defined in MythTV which is translated into the inpoint's and outpoint's of thecutlistfor the video. ↩ -
Realtime. ↩
-
After Post-processing. ↩
-
All can be combined with
ABR,Cut commercialsandsubtitlesselection. ↩ -
The stream used for this test was manually created for testing purposes. In the current version of the scripts all audio rendition are automatically created and shown in the UI. ↩
-
One minute of playback. ↩
-
Only VAAPI and no HW acceleration has been tested. Feedback on untested acceleration is appreciated. ↩
-
The channel information is extracted from MythTV automagically. ↩
-
FFmpeg encoding is stopped without checking if other users are watching the stream. ↩







