Browse Source

more segmenter v2 improvements (#1685)

* more segmenter v2 improvements

* changelog updates
pull/1686/head
Jason Dove 1 year ago committed by GitHub
parent
commit
35eb200aee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      CHANGELOG.md
  2. 10
      ErsatzTV.FFmpeg/OutputFormat/OutputFormatConcatHls.cs
  3. 12
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
  4. 31
      ErsatzTV/Controllers/IptvController.cs
  5. 15
      ErsatzTV/Startup.cs

10
CHANGELOG.md

@ -41,12 +41,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `Something.sdh.en.srt` - `Something.sdh.en.srt`
- `Something.en.forced.srt` - `Something.en.forced.srt`
- `Something.en.sdh.srt` - `Something.en.sdh.srt`
- Fix playback from Jellyfin 10.9 by allowing playlist HTTP HEAD requests
- Fix `HLS Segmenter V2` segment duration (previously 10s, now 4s)
### Changed ### Changed
- Use ffmpeg 7 in all docker images - Use ffmpeg 7 in all docker images
- Show health checks at top of home page; scroll release notes if needed - Show health checks at top of home page; scroll release notes if needed
- Improve `HLS Segmenter V2` compliance by serving fmp4 segments when `hevc` video format is selected - Improve `HLS Segmenter V2` compliance by:
- > 1.5. The container format for HEVC video MUST be fMP4. - Serving fmp4 segments when `hevc` video format is selected
- > 1.5. The container format for HEVC video MUST be fMP4.
- Using accurate BANDWIDTH value in multi-variant playlist
- Using proper MIME types for statically-served `.m3u8` and `.ts` files
- Serving playlists with gzip compression
## [0.8.6-beta] - 2024-04-03 ## [0.8.6-beta] - 2024-04-03
### Added ### Added

10
ErsatzTV.FFmpeg/OutputFormat/OutputFormatConcatHls.cs

@ -22,20 +22,17 @@ public class OutputFormatConcatHls : IPipelineStep
{ {
get get
{ {
string segmentType = "mpegts"; var segmentType = "mpegts";
string hlsFlags = "delete_segments+program_date_time+omit_endlist+discont_start+independent_segments";
// check for fmp4 output // check for fmp4 output
if (_segmentTemplate.Contains("m4s")) if (_segmentTemplate.Contains("m4s"))
{ {
segmentType = "fmp4"; segmentType = "fmp4";
hlsFlags = "delete_segments+program_date_time+omit_endlist";
} }
return return
[ [
//"-g", $"{gop}", "-g", $"{OutputFormatHls.SegmentSeconds}/2",
//"-keyint_min", $"{FRAME_RATE * OutputFormatHls.SegmentSeconds}",
"-force_key_frames", $"expr:gte(t,n_forced*{OutputFormatHls.SegmentSeconds}/2)", "-force_key_frames", $"expr:gte(t,n_forced*{OutputFormatHls.SegmentSeconds}/2)",
"-f", "hls", "-f", "hls",
"-hls_segment_type", segmentType, "-hls_segment_type", segmentType,
@ -45,7 +42,8 @@ public class OutputFormatConcatHls : IPipelineStep
"-hls_list_size", "25", // burst of 45 means ~12 segments, so allow that plus a handful "-hls_list_size", "25", // burst of 45 means ~12 segments, so allow that plus a handful
"-segment_list_flags", "+live", "-segment_list_flags", "+live",
"-hls_segment_filename", _segmentTemplate, "-hls_segment_filename", _segmentTemplate,
"-hls_flags", hlsFlags, "-hls_flags", "delete_segments+program_date_time+omit_endlist+discont_start+independent_segments",
"-master_pl_name", "playlist.m3u8",
_playlistPath _playlistPath
]; ];
} }

12
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

@ -147,7 +147,17 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
public FFmpegPipeline Build(FFmpegState ffmpegState, FrameState desiredState) public FFmpegPipeline Build(FFmpegState ffmpegState, FrameState desiredState)
{ {
OutputOption.OutputOption outputOption = new FastStartOutputOption(); OutputOption.OutputOption outputOption = new FastStartOutputOption();
if (ffmpegState.OutputFormat == OutputFormatKind.Mp4)
var isFmp4Hls = false;
if (ffmpegState.OutputFormat is OutputFormatKind.Hls)
{
foreach (string segmentTemplate in ffmpegState.HlsSegmentTemplate)
{
isFmp4Hls = segmentTemplate.Contains("m4s");
}
}
if (ffmpegState.OutputFormat == OutputFormatKind.Mp4 || isFmp4Hls)
{ {
outputOption = new Mp4OutputOptions(); outputOption = new Mp4OutputOptions();
} }

31
ErsatzTV/Controllers/IptvController.cs

@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.Text;
using CliWrap; using CliWrap;
using ErsatzTV.Application.Channels; using ErsatzTV.Application.Channels;
using ErsatzTV.Application.Images; using ErsatzTV.Application.Images;
@ -196,19 +197,20 @@ public class IptvController : ControllerBase
{ {
case "segmenter": case "segmenter":
case "segmenter-v2": case "segmenter-v2":
string multiVariantPlaylist = await GetMultiVariantPlaylist(channelNumber, mode);
_logger.LogDebug( _logger.LogDebug(
"Maybe starting ffmpeg session for channel {Channel}, mode {Mode}", "Maybe starting ffmpeg session for channel {Channel}, mode {Mode}",
channelNumber, channelNumber,
mode); mode);
var request = new StartFFmpegSession(channelNumber, mode, Request.Scheme, Request.Host.ToString()); var request = new StartFFmpegSession(channelNumber, mode, Request.Scheme, Request.Host.ToString());
Either<BaseError, Unit> result = await _mediator.Send(request); Either<BaseError, Unit> result = await _mediator.Send(request);
string multiVariantPlaylist = await GetMultiVariantPlaylist(channelNumber, mode);
return result.Match<IActionResult>( return result.Match<IActionResult>(
_ => _ =>
{ {
_logger.LogDebug( _logger.LogDebug(
"Session started; returning multi-variant playlist for channel {Channel}", "Session started; returning multi-variant playlist for channel {Channel}",
channelNumber); channelNumber);
return Content(multiVariantPlaylist, "application/vnd.apple.mpegurl"); return Content(multiVariantPlaylist, "application/vnd.apple.mpegurl");
// return Redirect($"~/iptv/session/{channelNumber}/hls.m3u8"); // return Redirect($"~/iptv/session/{channelNumber}/hls.m3u8");
}, },
@ -220,6 +222,7 @@ public class IptvController : ControllerBase
_logger.LogDebug( _logger.LogDebug(
"Session is already active; returning multi-variant playlist for channel {Channel}", "Session is already active; returning multi-variant playlist for channel {Channel}",
channelNumber); channelNumber);
return Content(multiVariantPlaylist, "application/vnd.apple.mpegurl"); return Content(multiVariantPlaylist, "application/vnd.apple.mpegurl");
// return RedirectPreserveMethod($"iptv/session/{channelNumber}/hls.m3u8"); // return RedirectPreserveMethod($"iptv/session/{channelNumber}/hls.m3u8");
default: default:
@ -266,6 +269,28 @@ public class IptvController : ControllerBase
_ => "hls.m3u8" _ => "hls.m3u8"
}; };
var variantPlaylist =
$"{Request.Scheme}://{Request.Host}/iptv/session/{channelNumber}/{file}{AccessTokenQuery()}";
try
{
if (mode == "segmenter-v2")
{
string fileName = Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "playlist.m3u8");
if (System.IO.File.Exists(fileName))
{
string text = await System.IO.File.ReadAllTextAsync(fileName, Encoding.UTF8);
return text.Replace("live.m3u8", variantPlaylist);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(
ex,
"Failed to return ffmpeg multi-variant playlist; falling back to generated playlist");
}
Option<ResolutionViewModel> maybeResolution = await _mediator.Send(new GetChannelResolution(channelNumber)); Option<ResolutionViewModel> maybeResolution = await _mediator.Send(new GetChannelResolution(channelNumber));
string resolution = string.Empty; string resolution = string.Empty;
foreach (ResolutionViewModel res in maybeResolution) foreach (ResolutionViewModel res in maybeResolution)
@ -275,8 +300,8 @@ public class IptvController : ControllerBase
return $@"#EXTM3U return $@"#EXTM3U
#EXT-X-VERSION:3 #EXT-X-VERSION:3
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=10000000{resolution} #EXT-X-STREAM-INF:BANDWIDTH=10000000{resolution}
{Request.Scheme}://{Request.Host}/iptv/session/{channelNumber}/{file}{AccessTokenQuery()}"; {variantPlaylist}";
} }
private string AccessTokenQuery() => string.IsNullOrWhiteSpace(Request.Query["access_token"]) private string AccessTokenQuery() => string.IsNullOrWhiteSpace(Request.Query["access_token"])

15
ErsatzTV/Startup.cs

@ -447,6 +447,11 @@ public class Startup
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.trakt.tv")); .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.trakt.tv"));
services.Configure<TraktConfiguration>(Configuration.GetSection("Trakt")); services.Configure<TraktConfiguration>(Configuration.GetSection("Trakt"));
services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
});
CustomServices(services); CustomServices(services);
} }
@ -515,7 +520,13 @@ public class Startup
app.UseStaticFiles(); app.UseStaticFiles();
var extensionProvider = new FileExtensionContentTypeProvider(); var extensionProvider = new FileExtensionContentTypeProvider();
extensionProvider.Mappings.Add(".m3u8", "application/x-mpegurl");
// fix static file M3U8 mime type
extensionProvider.Mappings.Add(".m3u8", "application/vnd.apple.mpegurl");
// fix static file TS mime type
extensionProvider.Mappings.Remove(".ts");
extensionProvider.Mappings.Add(".ts", "video/mp2t");
app.UseStaticFiles( app.UseStaticFiles(
new StaticFileOptions new StaticFileOptions
@ -534,6 +545,8 @@ public class Startup
ServeUnknownFileTypes = true ServeUnknownFileTypes = true
}); });
app.UseResponseCompression();
app.MapWhen( app.MapWhen(
ctx => !ctx.Request.Path.StartsWithSegments("/iptv"), ctx => !ctx.Request.Path.StartsWithSegments("/iptv"),
blazor => blazor =>

Loading…
Cancel
Save