using System; using System.Diagnostics; using System.IO; using System.Threading.Tasks; using ErsatzTV.Application.Channels.Queries; using ErsatzTV.Application.Images; using ErsatzTV.Application.Images.Queries; using ErsatzTV.Application.Streaming.Commands; using ErsatzTV.Application.Streaming.Queries; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Iptv; using LanguageExt; using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Unit = LanguageExt.Unit; namespace ErsatzTV.Controllers { [ApiController] [ApiExplorerSettings(IgnoreApi = true)] public class IptvController : ControllerBase { private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly ILogger _logger; private readonly IMediator _mediator; public IptvController( IMediator mediator, ILogger logger, IFFmpegSegmenterService ffmpegSegmenterService) { _mediator = mediator; _logger = logger; _ffmpegSegmenterService = ffmpegSegmenterService; } [HttpGet("iptv/channels.m3u")] public Task GetChannelPlaylist( [FromQuery] string mode = "mixed") => _mediator.Send(new GetChannelPlaylist(Request.Scheme, Request.Host.ToString(), mode)) .Map(Ok); [HttpGet("iptv/xmltv.xml")] public Task GetGuide() => _mediator.Send(new GetChannelGuide(Request.Scheme, Request.Host.ToString())) .Map(Ok); [HttpGet("iptv/channel/{channelNumber}.ts")] public async Task GetTransportStreamVideo( string channelNumber, [FromQuery] string mode = null) { FFmpegProcessRequest request = mode switch { "legacy" => new GetConcatProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber), _ => new GetWrappedProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber) }; return await _mediator.Send(request) .Map( result => result.Match( processModel => { Process process = processModel.Process; _logger.LogInformation("Starting ts stream for channel {ChannelNumber}", channelNumber); // _logger.LogDebug( // "ffmpeg concat arguments {FFmpegArguments}", // string.Join(" ", process.StartInfo.ArgumentList)); process.Start(); return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t"); }, error => BadRequest(error.Value))); } [HttpGet("iptv/session/{channelNumber}/hls.m3u8")] public async Task GetLivePlaylist(string channelNumber) { if (_ffmpegSegmenterService.SessionWorkers.TryGetValue(channelNumber, out IHlsSessionWorker worker)) { DateTimeOffset now = DateTimeOffset.Now.AddSeconds(-30); string fileName = Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live.m3u8"); if (System.IO.File.Exists(fileName)) { string[] input = await System.IO.File.ReadAllLinesAsync(fileName); TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(worker.PlaylistStart, now, input); return Content(result.Playlist, "application/vnd.apple.mpegurl"); } } return NotFound(); } [HttpGet("iptv/channel/{channelNumber}.m3u8")] public async Task GetHttpLiveStreamingVideo( string channelNumber, [FromQuery] string mode = "mixed") { switch (mode) { case "segmenter": Either result = await _mediator.Send(new StartFFmpegSession(channelNumber, false)); return result.Match( _ => Redirect($"/iptv/session/{channelNumber}/hls.m3u8"), error => { switch (error) { case ChannelSessionAlreadyActive: return RedirectPreserveMethod($"/iptv/session/{channelNumber}/hls.m3u8"); default: _logger.LogWarning( "Failed to start segmenter for channel {ChannelNumber}: {Error}", channelNumber, error.ToString()); return NotFound(); } }); default: return await _mediator.Send( new GetHlsPlaylistByChannelNumber( Request.Scheme, Request.Host.ToString(), channelNumber, mode)) .Map( r => r.Match( playlist => Content(playlist, "application/x-mpegurl"), error => BadRequest(error.Value))); } } [HttpGet("iptv/logos/{fileName}")] [HttpHead("iptv/logos/{fileName}.jpg")] [HttpGet("iptv/logos/{fileName}.jpg")] public async Task GetImage(string fileName) { Either cachedImagePath = await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Logo)); return cachedImagePath.Match( Left: _ => new NotFoundResult(), Right: r => new PhysicalFileResult(r.FileName, r.MimeType)); } } }