Browse Source

improve hls direct compatibility with channels dvr (#245)

* rename HttpLiveStreaming to HttpLiveStreamingDirect

* improve hls direct compatibility with channels dvr

* code cleanup
pull/246/head
Jason Dove 4 years ago committed by GitHub
parent
commit
0ef03d66f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs
  3. 29
      ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs
  4. 12
      ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs
  5. 2
      ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs
  6. 4
      ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumber.cs
  7. 46
      ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumberHandler.cs
  8. 2
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs
  9. 14
      ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs
  10. 2
      ErsatzTV.Core/Domain/StreamingMode.cs
  11. 2
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  12. 2
      ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
  13. 2
      ErsatzTV.Core/Hdhr/LineupItem.cs
  14. 2
      ErsatzTV.Core/Iptv/ChannelPlaylist.cs
  15. 2
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  16. 4
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  17. 6
      ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs
  18. 42
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs
  19. 7
      ErsatzTV/Controllers/InternalController.cs
  20. 18
      ErsatzTV/Controllers/IptvController.cs
  21. 6
      ErsatzTV/Pages/ChannelEditor.razor
  22. 2
      ErsatzTV/Pages/Channels.razor
  23. 6
      docs/user-guide/create-channels.md

9
CHANGELOG.md

@ -6,8 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -6,8 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Support `(Part #)` name suffixes for multi-part episode grouping
- Support multi-episode files in local libraries
- Support multi-episode files in local and Plex libraries
- Save Channels table page size
- Add optional query string parameter to M3U channel playlist to allow some customization per client
- `?mode=ts` will force `MPEG-TS` mode for all channels
- `?mode=hls-direct` will force `HLS Direct` mode for all channels
- `?mode=mixed` or no parameter will maintain existing behavior
### Changed
- Rename channel mode `TransportStream` to `MPEG-TS` and `HttpLiveStreaming` to `HLS Direct`
### Fixed
- Fix search result crashes due to missing season metadata

2
ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs

@ -3,5 +3,5 @@ using MediatR; @@ -3,5 +3,5 @@ using MediatR;
namespace ErsatzTV.Application.Channels.Queries
{
public record GetChannelPlaylist(string Scheme, string Host) : IRequest<ChannelPlaylist>;
public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest<ChannelPlaylist>;
}

29
ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
using System.Threading;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Iptv;
using LanguageExt;
@ -16,6 +18,31 @@ namespace ErsatzTV.Application.Channels.Queries @@ -16,6 +18,31 @@ namespace ErsatzTV.Application.Channels.Queries
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
.Map(channels => EnsureMode(channels, request.Mode))
.Map(channels => new ChannelPlaylist(request.Scheme, request.Host, channels));
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
{
var result = new List<Channel>();
foreach (Channel channel in channels)
{
switch (mode.ToLowerInvariant())
{
case "hls-direct":
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
result.Add(channel);
break;
case "ts":
channel.StreamingMode = StreamingMode.TransportStream;
result.Add(channel);
break;
default:
result.Add(channel);
break;
}
}
return result;
}
}
}

12
ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs

@ -39,6 +39,18 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -39,6 +39,18 @@ namespace ErsatzTV.Application.Streaming.Queries
private async Task<Validation<BaseError, Channel>> ChannelMustExist(T request) =>
(await _channelRepository.GetByNumber(request.ChannelNumber))
.Map(
channel =>
{
channel.StreamingMode = request.Mode.ToLowerInvariant() switch
{
"hls-direct" => StreamingMode.HttpLiveStreamingDirect,
"ts" => StreamingMode.TransportStream,
_ => channel.StreamingMode
};
return channel;
})
.ToValidation<BaseError>($"Channel number {request.ChannelNumber} does not exist.");
private Task<Validation<BaseError, string>> FFmpegPathMustExist() =>

2
ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs

@ -5,5 +5,5 @@ using MediatR; @@ -5,5 +5,5 @@ using MediatR;
namespace ErsatzTV.Application.Streaming.Queries
{
public record FFmpegProcessRequest(string ChannelNumber) : IRequest<Either<BaseError, Process>>;
public record FFmpegProcessRequest(string ChannelNumber, string Mode) : IRequest<Either<BaseError, Process>>;
}

4
ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumber.cs

@ -2,7 +2,9 @@ @@ -2,7 +2,9 @@
{
public record GetConcatProcessByChannelNumber : FFmpegProcessRequest
{
public GetConcatProcessByChannelNumber(string scheme, string host, string channelNumber) : base(channelNumber)
public GetConcatProcessByChannelNumber(string scheme, string host, string channelNumber) : base(
channelNumber,
"ts")
{
Scheme = scheme;
Host = host;

46
ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumberHandler.cs

@ -6,6 +6,7 @@ using ErsatzTV.Core.Domain; @@ -6,6 +6,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Caching.Memory;
using Serilog;
namespace ErsatzTV.Application.Streaming.Queries
@ -14,14 +15,17 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -14,14 +15,17 @@ namespace ErsatzTV.Application.Streaming.Queries
GetHlsPlaylistByChannelNumberHandler : IRequestHandler<GetHlsPlaylistByChannelNumber, Either<BaseError, string>>
{
private readonly IChannelRepository _channelRepository;
private readonly IMemoryCache _memoryCache;
private readonly IPlayoutRepository _playoutRepository;
public GetHlsPlaylistByChannelNumberHandler(
IChannelRepository channelRepository,
IPlayoutRepository playoutRepository)
IPlayoutRepository playoutRepository,
IMemoryCache memoryCache)
{
_channelRepository = channelRepository;
_playoutRepository = playoutRepository;
_memoryCache = memoryCache;
}
public Task<Either<BaseError, string>> Handle(
@ -40,12 +44,15 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -40,12 +44,15 @@ namespace ErsatzTV.Application.Streaming.Queries
return maybePlayoutItem.Match<Either<BaseError, string>>(
playoutItem =>
{
double timeRemaining = Math.Abs((playoutItem.Finish - now).TotalSeconds);
long index = GetIndexForChannel(channel, playoutItem);
double timeRemaining = Math.Abs((playoutItem.FinishOffset - now).TotalSeconds);
return $@"#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:18000
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:{index}
#EXT-X-DISCONTINUITY
#EXTINF:{timeRemaining:F2},
{request.Scheme}://{request.Host}/ffmpeg/stream/{request.ChannelNumber}
{request.Scheme}://{request.Host}/ffmpeg/stream/{request.ChannelNumber}?index={index}&mode=hls-direct
";
},
() =>
@ -59,5 +66,36 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -59,5 +66,36 @@ namespace ErsatzTV.Application.Streaming.Queries
private async Task<Validation<BaseError, Channel>> ChannelMustExist(GetHlsPlaylistByChannelNumber request) =>
(await _channelRepository.GetByNumber(request.ChannelNumber))
.ToValidation<BaseError>($"Channel number {request.ChannelNumber} does not exist.");
private long GetIndexForChannel(Channel channel, PlayoutItem playoutItem)
{
long ticks = playoutItem.Start.Ticks;
var key = new ChannelIndexKey(channel.Id);
long index;
if (_memoryCache.TryGetValue(key, out ChannelIndexRecord channelRecord))
{
if (channelRecord.StartTicks == ticks)
{
index = channelRecord.Index;
}
else
{
index = channelRecord.Index + 1;
_memoryCache.Set(key, new ChannelIndexRecord(ticks, index), TimeSpan.FromDays(1));
}
}
else
{
index = 1;
_memoryCache.Set(key, new ChannelIndexRecord(ticks, index), TimeSpan.FromDays(1));
}
return index;
}
private record ChannelIndexKey(int ChannelId);
private record ChannelIndexRecord(long StartTicks, long Index);
}
}

2
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
{
public record GetPlayoutItemProcessByChannelNumber : FFmpegProcessRequest
{
public GetPlayoutItemProcessByChannelNumber(string channelNumber) : base(channelNumber)
public GetPlayoutItemProcessByChannelNumber(string channelNumber, string mode) : base(channelNumber, mode)
{
}
}

14
ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs

@ -40,7 +40,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -40,7 +40,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 };
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming,
StreamingMode.HttpLiveStreamingDirect,
ffmpegProfile,
new MediaVersion(),
new MediaStream(),
@ -76,7 +76,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -76,7 +76,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FFmpegProfile ffmpegProfile = TestProfile();
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming,
StreamingMode.HttpLiveStreamingDirect,
ffmpegProfile,
new MediaVersion(),
new MediaStream(),
@ -112,7 +112,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -112,7 +112,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FFmpegProfile ffmpegProfile = TestProfile();
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming,
StreamingMode.HttpLiveStreamingDirect,
ffmpegProfile,
new MediaVersion(),
new MediaStream(),
@ -151,7 +151,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -151,7 +151,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FFmpegProfile ffmpegProfile = TestProfile();
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming,
StreamingMode.HttpLiveStreamingDirect,
ffmpegProfile,
new MediaVersion(),
new MediaStream(),
@ -317,7 +317,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -317,7 +317,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" };
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming,
StreamingMode.HttpLiveStreamingDirect,
ffmpegProfile,
version,
new MediaStream(),
@ -426,7 +426,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -426,7 +426,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming,
StreamingMode.HttpLiveStreamingDirect,
ffmpegProfile,
version,
new MediaStream { Codec = "mpeg2video" },
@ -718,7 +718,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -718,7 +718,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
var version = new MediaVersion();
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming,
StreamingMode.HttpLiveStreamingDirect,
ffmpegProfile,
version,
new MediaStream(),

2
ErsatzTV.Core/Domain/StreamingMode.cs

@ -3,6 +3,6 @@ @@ -3,6 +3,6 @@
public enum StreamingMode
{
TransportStream = 1,
HttpLiveStreaming = 2
HttpLiveStreamingDirect = 2
}
}

2
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -64,7 +64,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -64,7 +64,7 @@ namespace ErsatzTV.Core.FFmpeg
switch (streamingMode)
{
case StreamingMode.HttpLiveStreaming:
case StreamingMode.HttpLiveStreamingDirect:
result.AudioCodec = "copy";
result.VideoCodec = "copy";
result.Deinterlace = false;

2
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -28,7 +28,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -28,7 +28,7 @@ namespace ErsatzTV.Core.FFmpeg
public async Task<Option<MediaStream>> SelectAudioStream(Channel channel, MediaVersion version)
{
if (channel.StreamingMode == StreamingMode.HttpLiveStreaming &&
if (channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect &&
string.IsNullOrWhiteSpace(channel.PreferredLanguageCode))
{
_logger.LogDebug(

2
ErsatzTV.Core/Hdhr/LineupItem.cs

@ -22,7 +22,7 @@ namespace ErsatzTV.Core.Hdhr @@ -22,7 +22,7 @@ namespace ErsatzTV.Core.Hdhr
public string URL => _channel.StreamingMode switch
{
StreamingMode.HttpLiveStreaming => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.m3u8",
StreamingMode.HttpLiveStreamingDirect => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.m3u8",
_ => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.ts"
};
}

2
ErsatzTV.Core/Iptv/ChannelPlaylist.cs

@ -42,7 +42,7 @@ namespace ErsatzTV.Core.Iptv @@ -42,7 +42,7 @@ namespace ErsatzTV.Core.Iptv
string format = channel.StreamingMode switch
{
StreamingMode.HttpLiveStreaming => "m3u8",
StreamingMode.HttpLiveStreamingDirect => "m3u8",
_ => "ts"
};

2
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -177,7 +177,7 @@ namespace ErsatzTV.Core.Metadata @@ -177,7 +177,7 @@ namespace ErsatzTV.Core.Metadata
private async Task<bool> ApplyMetadataUpdate(Episode episode, List<EpisodeMetadata> episodeMetadata)
{
var updated = false;
episode.EpisodeMetadata ??= new List<EpisodeMetadata>();
var toUpdate = episode.EpisodeMetadata

4
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -22,9 +22,9 @@ namespace ErsatzTV.Core.Metadata @@ -22,9 +22,9 @@ namespace ErsatzTV.Core.Metadata
private readonly ILibraryRepository _libraryRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly IMetadataRepository _metadataRepository;
private readonly ILogger<TelevisionFolderScanner> _logger;
private readonly IMediator _mediator;
private readonly IMetadataRepository _metadataRepository;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ITelevisionRepository _televisionRepository;
@ -276,7 +276,7 @@ namespace ErsatzTV.Core.Metadata @@ -276,7 +276,7 @@ namespace ErsatzTV.Core.Metadata
private async Task<Either<BaseError, Season>> EnsureMetadataExists(Season season)
{
season.SeasonMetadata ??= new List<SeasonMetadata>();
if (!season.SeasonMetadata.Any())
{
var metadata = new SeasonMetadata

6
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -428,12 +428,12 @@ namespace ErsatzTV.Core.Plex @@ -428,12 +428,12 @@ namespace ErsatzTV.Core.Plex
var toAdd = incoming.EpisodeMetadata
.Where(em => existing.EpisodeMetadata.All(em2 => em2.EpisodeNumber != em.EpisodeNumber))
.ToList();
foreach (EpisodeMetadata metadata in toRemove)
{
await _televisionRepository.RemoveMetadata(existing, metadata);
}
foreach (EpisodeMetadata metadata in toAdd)
{
metadata.EpisodeId = existing.Id;
@ -442,7 +442,7 @@ namespace ErsatzTV.Core.Plex @@ -442,7 +442,7 @@ namespace ErsatzTV.Core.Plex
await _metadataRepository.Add(metadata);
}
// TODO: update existing metadata
return existing;

42
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -119,27 +119,6 @@ namespace ErsatzTV.Infrastructure.Plex @@ -119,27 +119,6 @@ namespace ErsatzTV.Infrastructure.Plex
}
}
private List<PlexEpisode> ProcessMultiEpisodeFiles(IEnumerable<PlexEpisode> episodes)
{
// add all metadata from duplicate paths to first entry with given path
// i.e. s1e1 episode will add s1e2 metadata if s1e1 and s1e2 have same physical path
var result = new Dictionary<string, PlexEpisode>();
foreach (PlexEpisode episode in episodes.OrderBy(e => e.EpisodeMetadata.Head().EpisodeNumber))
{
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
if (result.TryGetValue(path, out PlexEpisode existing))
{
existing.EpisodeMetadata.Add(episode.EpisodeMetadata.Head());
}
else
{
result.Add(path, episode);
}
}
return result.Values.ToList();
}
public async Task<Either<BaseError, MovieMetadata>> GetMovieMetadata(
PlexLibrary library,
string key,
@ -247,6 +226,27 @@ namespace ErsatzTV.Infrastructure.Plex @@ -247,6 +226,27 @@ namespace ErsatzTV.Infrastructure.Plex
}
}
private List<PlexEpisode> ProcessMultiEpisodeFiles(IEnumerable<PlexEpisode> episodes)
{
// add all metadata from duplicate paths to first entry with given path
// i.e. s1e1 episode will add s1e2 metadata if s1e1 and s1e2 have same physical path
var result = new Dictionary<string, PlexEpisode>();
foreach (PlexEpisode episode in episodes.OrderBy(e => e.EpisodeMetadata.Head().EpisodeNumber))
{
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
if (result.TryGetValue(path, out PlexEpisode existing))
{
existing.EpisodeMetadata.Add(episode.EpisodeMetadata.Head());
}
else
{
result.Add(path, episode);
}
}
return result.Values.ToList();
}
private static IPlexServerApi XmlServiceFor(string uri)
{
var overrides = new XmlAttributeOverrides();

7
ErsatzTV/Controllers/InternalController.cs

@ -27,8 +27,11 @@ namespace ErsatzTV.Controllers @@ -27,8 +27,11 @@ namespace ErsatzTV.Controllers
.ToActionResult();
[HttpGet("ffmpeg/stream/{channelNumber}")]
public Task<IActionResult> GetStream(string channelNumber) =>
_mediator.Send(new GetPlayoutItemProcessByChannelNumber(channelNumber)).Map(
public Task<IActionResult> GetStream(
string channelNumber,
[FromQuery]
string mode = "mixed") =>
_mediator.Send(new GetPlayoutItemProcessByChannelNumber(channelNumber, mode)).Map(
result =>
result.Match<IActionResult>(
process =>

18
ErsatzTV/Controllers/IptvController.cs

@ -27,8 +27,10 @@ namespace ErsatzTV.Controllers @@ -27,8 +27,10 @@ namespace ErsatzTV.Controllers
}
[HttpGet("iptv/channels.m3u")]
public Task<IActionResult> GetChannelPlaylist() =>
_mediator.Send(new GetChannelPlaylist(Request.Scheme, Request.Host.ToString()))
public Task<IActionResult> GetChannelPlaylist(
[FromQuery]
string mode = "mixed") =>
_mediator.Send(new GetChannelPlaylist(Request.Scheme, Request.Host.ToString(), mode))
.Map<ChannelPlaylist, IActionResult>(Ok);
[HttpGet("iptv/xmltv.xml")]
@ -54,14 +56,14 @@ namespace ErsatzTV.Controllers @@ -54,14 +56,14 @@ namespace ErsatzTV.Controllers
[HttpGet("iptv/channel/{channelNumber}.m3u8")]
public Task<IActionResult> GetHttpLiveStreamingVideo(string channelNumber) =>
_mediator.Send(new GetHlsPlaylistByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber))
_mediator.Send(
new GetHlsPlaylistByChannelNumber(
Request.Scheme,
Request.Host.ToString(),
channelNumber))
.Map(
result => result.Match<IActionResult>(
playlist =>
{
_logger.LogInformation("Starting hls stream for channel {ChannelNumber}", channelNumber);
return Content(playlist, "application/x-mpegurl");
},
playlist => Content(playlist, "application/x-mpegurl"),
error => BadRequest(error.Value)));
[HttpGet("iptv/logos/{fileName}")]

6
ErsatzTV/Pages/ChannelEditor.razor

@ -24,10 +24,8 @@ @@ -24,10 +24,8 @@
<MudTextField Label="Number" @bind-Value="_model.Number" For="@(() => _model.Number)" Immediate="true"/>
<MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/>
<MudSelect Class="mt-3" Label="Streaming Mode" @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)">
@foreach (StreamingMode streamingMode in Enum.GetValues<StreamingMode>())
{
<MudSelectItem Value="@streamingMode">@streamingMode</MudSelectItem>
}
<MudSelectItem Value="@(StreamingMode.TransportStream)">MPEG-TS</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingDirect)">HLS Direct</MudSelectItem>
</MudSelect>
<MudSelect Class="mt-3" Label="FFmpeg Profile" @bind-Value="_model.FFmpegProfileId" For="@(() => _model.FFmpegProfileId)"
Disabled="@(_model.StreamingMode != StreamingMode.TransportStream)">

2
ErsatzTV/Pages/Channels.razor

@ -50,7 +50,7 @@ @@ -50,7 +50,7 @@
</MudTd>
<MudTd DataLabel="Name">@context.Name</MudTd>
<MudTd DataLabel="Language">@context.PreferredLanguageCode</MudTd>
<MudTd DataLabel="Mode">@(context.StreamingMode == StreamingMode.TransportStream ? "TS" : "HLS")</MudTd>
<MudTd DataLabel="Mode">@(context.StreamingMode == StreamingMode.TransportStream ? "MPEG-TS" : "HLS Direct")</MudTd>
<MudTd DataLabel="FFmpeg Profile">
@if (context.StreamingMode == StreamingMode.TransportStream)
{

6
docs/user-guide/create-channels.md

@ -8,9 +8,9 @@ Channel numbers can be whole numbers or can contain one decimal, like `500` or ` @@ -8,9 +8,9 @@ Channel numbers can be whole numbers or can contain one decimal, like `500` or `
### Streaming Mode
Two streaming modes are currently supported: `Transport Stream` and `HttpLiveStreaming`.
`Transport Stream` is considered stable and is recommended for most purposes.
`HttpLiveStreaming` is unstable and is not recommended for general use.
Two streaming modes are currently supported: `MPEG-TS` (Transport Stream) and `HLS Direct` (HTTP Live Streaming Direct).
`MPEG-TS` is considered stable and is recommended for most purposes.
`HLS Direct` is unstable and is not recommended for general use, but can avoid the need to transcode with some clients.
### FFmpeg Profile

Loading…
Cancel
Save