@page "/channels" @using System.Globalization @using ErsatzTV.Application.Channels @using ErsatzTV.Application.Configuration @using ErsatzTV.Application.FFmpegProfiles @using ErsatzTV.Core.Interfaces.FFmpeg @implements IDisposable @inject IDialogService Dialog @inject IMediator Mediator @inject NavigationManager NavigationManager @inject IFFmpegSegmenterService SegmenterService Add Channel
Channels Number Logo Name Language Mode FFmpeg Profile @context.Number @if (!string.IsNullOrWhiteSpace(context.Logo?.Path)) { } else { } @context.Name @context.PreferredAudioLanguageCode @GetStreamingMode(context.StreamingMode) @if (context.StreamingMode != StreamingMode.HttpLiveStreamingDirect) { @_ffmpegProfiles.Find(p => p.Id == context.FFmpegProfileId)?.Name }
@if (CanPreviewChannel(context)) { } else { } @if (SegmenterService.IsActive(context.Number)) { } else {
}
@code { private readonly CancellationTokenSource _cts = new(); private MudTable _table; private List _ffmpegProfiles = new(); private int _rowsPerPage = 10; protected override void OnInitialized() => SegmenterService.OnWorkersChanged += WorkersChanged; private void WorkersChanged(object sender, EventArgs e) => InvokeAsync(StateHasChanged); public void Dispose() { SegmenterService.OnWorkersChanged -= WorkersChanged; _cts.Cancel(); _cts.Dispose(); } protected override async Task OnParametersSetAsync() { _ffmpegProfiles = await Mediator.Send(new GetAllFFmpegProfiles(), _cts.Token); _rowsPerPage = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.ChannelsPageSize), _cts.Token) .Map(maybeRows => maybeRows.Match(ce => int.TryParse(ce.Value, out int rows) ? rows : 10, () => 10)); } private async Task StopChannel(ChannelViewModel channel) => await SegmenterService.StopChannel(channel.Number, _cts.Token); private bool CanPreviewChannel(ChannelViewModel channel) { if (!channel.IsEnabled) { return false; } if (channel.StreamingMode is StreamingMode.HttpLiveStreamingDirect or StreamingMode.TransportStream) { return false; } if (channel.PlayoutCount < 1) { return false; } Option maybeProfile = Optional(_ffmpegProfiles.Find(p => p.Id == channel.FFmpegProfileId)); foreach (FFmpegProfileViewModel profile in maybeProfile) { return profile.VideoFormat is FFmpegProfileVideoFormat.H264 && profile.AudioFormat is FFmpegProfileAudioFormat.Aac; } return false; } private async Task PreviewChannel(ChannelViewModel channel) { if (!CanPreviewChannel(channel)) { return; } Option maybeProfile = Optional(_ffmpegProfiles.Find(p => p.Id == channel.FFmpegProfileId)); foreach (FFmpegProfileViewModel profile in maybeProfile) { if (profile.VideoFormat == FFmpegProfileVideoFormat.Hevc) { return; } var uri = new UriBuilder(NavigationManager.ToAbsoluteUri(NavigationManager.Uri)); uri.Path = uri.Path.Replace("/channels", $"/iptv/channel/{channel.Number}.m3u8"); uri.Query = channel.StreamingMode switch { StreamingMode.HttpLiveStreamingSegmenterV2 => "?mode=segmenter-v2", _ => "?mode=segmenter" }; if (JwtHelper.IsEnabled) { uri.Query += $"&access_token={JwtHelper.GenerateToken()}"; } var parameters = new DialogParameters { { "StreamUri", uri.ToString() } }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraLarge }; await Dialog.ShowAsync("Channel Preview", parameters, options); } } private async Task DeleteChannelAsync(ChannelViewModel channel) { var parameters = new DialogParameters { { "EntityType", "channel" }, { "EntityName", channel.Name } }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; IDialogReference dialog = await Dialog.ShowAsync("Delete Channel", parameters, options); DialogResult result = await dialog.Result; if (result is { Canceled: false }) { await Mediator.Send(new DeleteChannel(channel.Id), _cts.Token); if (_table != null) { await _table.ReloadServerData(); } } } private async Task> ServerReload(TableState state, CancellationToken cancellationToken) { await Mediator.Send(new SaveConfigElementByKey(ConfigElementKey.ChannelsPageSize, state.PageSize.ToString()), _cts.Token); List channels = await Mediator.Send(new GetAllChannels(), _cts.Token); IOrderedEnumerable sorted = channels.OrderBy(c => decimal.Parse(c.Number, CultureInfo.InvariantCulture)); CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); var processedChannels = new List(); foreach (ChannelViewModel channel in sorted) { Option maybeCultureInfo = allCultures.Find(ci => string.Equals( ci.ThreeLetterISOLanguageName, channel.PreferredAudioLanguageCode, StringComparison.OrdinalIgnoreCase)); maybeCultureInfo.Match( cultureInfo => processedChannels.Add(channel with { PreferredAudioLanguageCode = cultureInfo.EnglishName }), () => processedChannels.Add(channel)); } // TODO: properly page this data return new TableData { TotalItems = channels.Count, Items = processedChannels.Skip(state.Page * state.PageSize).Take(state.PageSize) }; } private static string GetStreamingMode(StreamingMode streamingMode) => streamingMode switch { StreamingMode.HttpLiveStreamingDirect => "HLS Direct", StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter", StreamingMode.HttpLiveStreamingSegmenterV2 => "HLS Segmenter V2", StreamingMode.TransportStreamHybrid => "MPEG-TS", _ => "MPEG-TS (Legacy)" }; }