playback troubleshooting improvements (#2157)
This commit is contained in:
+1
-1
@@ -90,7 +90,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
- The media item id (found in ETV media info, and ETV movie URLs)
|
- The media item id (found in ETV media info, and ETV movie URLs)
|
||||||
- The ffmpeg profile to use
|
- The ffmpeg profile to use
|
||||||
- The watermark to use (if any)
|
- The watermark to use (if any)
|
||||||
- Clicking `Play` will play the specified content using the desired settings
|
- Clicking `Play` will play up to 30 seconds of the specified content using the desired settings
|
||||||
- Clicking `Download Results` will generate a zip archive containing:
|
- Clicking `Download Results` will generate a zip archive containing:
|
||||||
- The FFmpeg report of the playback attempt
|
- The FFmpeg report of the playback attempt
|
||||||
- The media info for the content
|
- The media info for the content
|
||||||
|
|||||||
+14
-47
@@ -1,68 +1,35 @@
|
|||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using ErsatzTV.Application.MediaItems;
|
|
||||||
using ErsatzTV.Application.Troubleshooting.Queries;
|
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Troubleshooting;
|
namespace ErsatzTV.Application.Troubleshooting;
|
||||||
|
|
||||||
public class ArchiveTroubleshootingResultsHandler(IMediator mediator, ILocalFileSystem localFileSystem)
|
public class ArchiveTroubleshootingResultsHandler(ILocalFileSystem localFileSystem)
|
||||||
: IRequestHandler<ArchiveTroubleshootingResults, Option<string>>
|
: IRequestHandler<ArchiveTroubleshootingResults, Option<string>>
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions Options = new()
|
public Task<Option<string>> Handle(ArchiveTroubleshootingResults request, CancellationToken cancellationToken)
|
||||||
{
|
|
||||||
Converters = { new JsonStringEnumConverter() },
|
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
||||||
WriteIndented = true
|
|
||||||
};
|
|
||||||
|
|
||||||
public async Task<Option<string>> Handle(ArchiveTroubleshootingResults request, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
string tempFile = Path.GetTempFileName();
|
string tempFile = Path.GetTempFileName();
|
||||||
using ZipArchive zipArchive = ZipFile.Open(tempFile, ZipArchiveMode.Update);
|
using ZipArchive zipArchive = ZipFile.Open(tempFile, ZipArchiveMode.Update);
|
||||||
|
|
||||||
string transcodeFolder = Path.Combine(FileSystemLayout.TranscodeFolder, ".troubleshooting");
|
var hasReport = false;
|
||||||
|
foreach (string file in localFileSystem.ListFiles(FileSystemLayout.TranscodeTroubleshootingFolder))
|
||||||
bool hasReport = false;
|
|
||||||
foreach (string file in localFileSystem.ListFiles(transcodeFolder))
|
|
||||||
{
|
{
|
||||||
|
string fileName = Path.GetFileName(file);
|
||||||
|
|
||||||
// add to archive
|
// add to archive
|
||||||
if (Path.GetFileName(file).StartsWith("ffmpeg-", StringComparison.InvariantCultureIgnoreCase))
|
if (fileName.StartsWith("ffmpeg-", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
hasReport = true;
|
hasReport = true;
|
||||||
zipArchive.CreateEntryFromFile(file, Path.GetFileName(file));
|
zipArchive.CreateEntryFromFile(file, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Path.GetExtension(file).Equals(".json", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
zipArchive.CreateEntryFromFile(file, fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Either<BaseError, MediaItemInfo> maybeMediaItemInfo = await mediator.Send(new GetMediaItemInfo(request.MediaItemId), cancellationToken);
|
return Task.FromResult(hasReport ? tempFile : Option<string>.None);
|
||||||
foreach (MediaItemInfo info in maybeMediaItemInfo.RightToSeq())
|
|
||||||
{
|
|
||||||
string infoJson = JsonSerializer.Serialize(info, Options);
|
|
||||||
string tempMediaInfoFile = Path.GetTempFileName();
|
|
||||||
await File.WriteAllTextAsync(tempMediaInfoFile, infoJson, cancellationToken);
|
|
||||||
zipArchive.CreateEntryFromFile(tempMediaInfoFile, "media_info.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
TroubleshootingInfo troubleshootingInfo = await mediator.Send(new GetTroubleshootingInfo(), cancellationToken);
|
|
||||||
|
|
||||||
string troubleshootingInfoJson = JsonSerializer.Serialize(
|
|
||||||
new
|
|
||||||
{
|
|
||||||
troubleshootingInfo.Version,
|
|
||||||
Environment = troubleshootingInfo.Environment.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value),
|
|
||||||
troubleshootingInfo.Health,
|
|
||||||
troubleshootingInfo.FFmpegSettings,
|
|
||||||
troubleshootingInfo.Channels,
|
|
||||||
troubleshootingInfo.FFmpegProfiles
|
|
||||||
},
|
|
||||||
Options);
|
|
||||||
|
|
||||||
string tempTroubleshootingInfoFile = Path.GetTempFileName();
|
|
||||||
await File.WriteAllTextAsync(tempTroubleshootingInfoFile, troubleshootingInfoJson, cancellationToken);
|
|
||||||
zipArchive.CreateEntryFromFile(tempTroubleshootingInfoFile, "troubleshooting_info.json");
|
|
||||||
|
|
||||||
return hasReport ? tempFile : Option<string>.None;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-4
@@ -9,6 +9,7 @@ using ErsatzTV.Core.FFmpeg;
|
|||||||
using ErsatzTV.Core.Interfaces.Emby;
|
using ErsatzTV.Core.Interfaces.Emby;
|
||||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||||
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Plex;
|
using ErsatzTV.Core.Interfaces.Plex;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
@@ -25,6 +26,7 @@ public class PrepareTroubleshootingPlaybackHandler(
|
|||||||
IEmbyPathReplacementService embyPathReplacementService,
|
IEmbyPathReplacementService embyPathReplacementService,
|
||||||
IFFmpegProcessService ffmpegProcessService,
|
IFFmpegProcessService ffmpegProcessService,
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
|
IEntityLocker entityLocker,
|
||||||
ILogger<PrepareTroubleshootingPlaybackHandler> logger)
|
ILogger<PrepareTroubleshootingPlaybackHandler> logger)
|
||||||
: IRequestHandler<PrepareTroubleshootingPlayback, Either<BaseError, Command>>
|
: IRequestHandler<PrepareTroubleshootingPlayback, Either<BaseError, Command>>
|
||||||
{
|
{
|
||||||
@@ -45,10 +47,15 @@ public class PrepareTroubleshootingPlaybackHandler(
|
|||||||
string ffprobePath,
|
string ffprobePath,
|
||||||
FFmpegProfile ffmpegProfile)
|
FFmpegProfile ffmpegProfile)
|
||||||
{
|
{
|
||||||
string transcodeFolder = Path.Combine(FileSystemLayout.TranscodeFolder, ".troubleshooting");
|
if (entityLocker.IsTroubleshootingPlaybackLocked())
|
||||||
|
{
|
||||||
|
return BaseError.New("Troubleshooting playback is locked");
|
||||||
|
}
|
||||||
|
|
||||||
localFileSystem.EnsureFolderExists(transcodeFolder);
|
entityLocker.LockTroubleshootingPlayback();
|
||||||
localFileSystem.EmptyFolder(transcodeFolder);
|
|
||||||
|
localFileSystem.EnsureFolderExists(FileSystemLayout.TranscodeTroubleshootingFolder);
|
||||||
|
localFileSystem.EmptyFolder(FileSystemLayout.TranscodeTroubleshootingFolder);
|
||||||
|
|
||||||
ChannelSubtitleMode subtitleMode = ChannelSubtitleMode.None;
|
ChannelSubtitleMode subtitleMode = ChannelSubtitleMode.None;
|
||||||
|
|
||||||
@@ -108,7 +115,7 @@ public class PrepareTroubleshootingPlaybackHandler(
|
|||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
transcodeFolder,
|
FileSystemLayout.TranscodeTroubleshootingFolder,
|
||||||
_ => { });
|
_ => { });
|
||||||
|
|
||||||
return process;
|
return process;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
using CliWrap;
|
using CliWrap;
|
||||||
|
using ErsatzTV.Application.MediaItems;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Troubleshooting;
|
namespace ErsatzTV.Application.Troubleshooting;
|
||||||
|
|
||||||
public record StartTroubleshootingPlayback(Command Command) : IRequest, IFFmpegWorkerRequest;
|
public record StartTroubleshootingPlayback(
|
||||||
|
Command Command,
|
||||||
|
MediaItemInfo MediaItemInfo,
|
||||||
|
TroubleshootingInfo TroubleshootingInfo) : IRequest, IFFmpegWorkerRequest;
|
||||||
|
|||||||
+53
-15
@@ -1,33 +1,71 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using CliWrap;
|
using CliWrap;
|
||||||
using CliWrap.Buffered;
|
using CliWrap.Buffered;
|
||||||
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Interfaces.Locking;
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
|
using ErsatzTV.Core.Notifications;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Troubleshooting;
|
namespace ErsatzTV.Application.Troubleshooting;
|
||||||
|
|
||||||
public class StartTroubleshootingPlaybackHandler(
|
public class StartTroubleshootingPlaybackHandler(
|
||||||
|
IMediator mediator,
|
||||||
IEntityLocker entityLocker,
|
IEntityLocker entityLocker,
|
||||||
ILogger<StartTroubleshootingPlaybackHandler> logger)
|
ILogger<StartTroubleshootingPlaybackHandler> logger)
|
||||||
: IRequestHandler<StartTroubleshootingPlayback>
|
: IRequestHandler<StartTroubleshootingPlayback>
|
||||||
{
|
{
|
||||||
|
private static readonly JsonSerializerOptions Options = new()
|
||||||
|
{
|
||||||
|
Converters = { new JsonStringEnumConverter() },
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
public async Task Handle(StartTroubleshootingPlayback request, CancellationToken cancellationToken)
|
public async Task Handle(StartTroubleshootingPlayback request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
logger.LogDebug("ffmpeg troubleshooting arguments {FFmpegArguments}", request.Command.Arguments);
|
try
|
||||||
|
|
||||||
BufferedCommandResult result = await request.Command
|
|
||||||
.WithValidation(CommandResultValidation.None)
|
|
||||||
.ExecuteBufferedAsync(cancellationToken);
|
|
||||||
|
|
||||||
entityLocker.UnlockTroubleshootingPlayback();
|
|
||||||
|
|
||||||
|
|
||||||
logger.LogInformation("Troubleshooting playback completed with exit code {ExitCode}", result.ExitCode);
|
|
||||||
|
|
||||||
foreach (KeyValuePair<string, string> env in request.Command.EnvironmentVariables)
|
|
||||||
{
|
{
|
||||||
logger.LogInformation("{Key} => {Value}", env.Key, env.Value);
|
// write media info without title
|
||||||
}
|
string infoJson = JsonSerializer.Serialize(request.MediaItemInfo with { Title = null }, Options);
|
||||||
|
await File.WriteAllTextAsync(
|
||||||
|
Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "media_info.json"),
|
||||||
|
infoJson,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
// TODO: something with the result ???
|
// write troubleshooting info
|
||||||
|
string troubleshootingInfoJson = JsonSerializer.Serialize(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
request.TroubleshootingInfo.Version,
|
||||||
|
Environment = request.TroubleshootingInfo.Environment.OrderBy(x => x.Key)
|
||||||
|
.ToDictionary(x => x.Key, x => x.Value),
|
||||||
|
request.TroubleshootingInfo.Health,
|
||||||
|
request.TroubleshootingInfo.FFmpegSettings,
|
||||||
|
request.TroubleshootingInfo.FFmpegProfiles,
|
||||||
|
request.TroubleshootingInfo.Watermarks
|
||||||
|
},
|
||||||
|
Options);
|
||||||
|
await File.WriteAllTextAsync(
|
||||||
|
Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "troubleshooting_info.json"),
|
||||||
|
troubleshootingInfoJson,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
logger.LogDebug("ffmpeg troubleshooting arguments {FFmpegArguments}", request.Command.Arguments);
|
||||||
|
|
||||||
|
BufferedCommandResult result = await request.Command
|
||||||
|
.WithValidation(CommandResultValidation.None)
|
||||||
|
.ExecuteBufferedAsync(cancellationToken);
|
||||||
|
|
||||||
|
await mediator.Publish(
|
||||||
|
new PlaybackTroubleshootingCompletedNotification(result.ExitCode),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
logger.LogDebug("Troubleshooting playback completed with exit code {ExitCode}", result.ExitCode);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
entityLocker.UnlockTroubleshootingPlayback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
|
|||||||
.Filter(f => channelFFmpegProfiles.Contains(f.Id))
|
.Filter(f => channelFFmpegProfiles.Contains(f.Id))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
List<ChannelWatermark> channelWatermarks = await dbContext.ChannelWatermarks
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
string nvidiaCapabilities = null;
|
string nvidiaCapabilities = null;
|
||||||
string qsvCapabilities = null;
|
string qsvCapabilities = null;
|
||||||
string vaapiCapabilities = null;
|
string vaapiCapabilities = null;
|
||||||
@@ -155,6 +159,7 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
|
|||||||
ffmpegSettings,
|
ffmpegSettings,
|
||||||
activeFFmpegProfiles,
|
activeFFmpegProfiles,
|
||||||
channels,
|
channels,
|
||||||
|
channelWatermarks,
|
||||||
nvidiaCapabilities,
|
nvidiaCapabilities,
|
||||||
qsvCapabilities,
|
qsvCapabilities,
|
||||||
vaapiCapabilities);
|
vaapiCapabilities);
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ namespace ErsatzTV.Application.Troubleshooting;
|
|||||||
public record TroubleshootingInfo(
|
public record TroubleshootingInfo(
|
||||||
string Version,
|
string Version,
|
||||||
Dictionary<string, string> Environment,
|
Dictionary<string, string> Environment,
|
||||||
IEnumerable<HealthCheckResultSummary> Health,
|
List<HealthCheckResultSummary> Health,
|
||||||
FFmpegSettingsViewModel FFmpegSettings,
|
FFmpegSettingsViewModel FFmpegSettings,
|
||||||
IEnumerable<FFmpegProfile> FFmpegProfiles,
|
List<FFmpegProfile> FFmpegProfiles,
|
||||||
IEnumerable<Channel> Channels,
|
List<Channel> Channels,
|
||||||
|
List<ChannelWatermark> Watermarks,
|
||||||
string NvidiaCapabilities,
|
string NvidiaCapabilities,
|
||||||
string QsvCapabilities,
|
string QsvCapabilities,
|
||||||
string VaapiCapabilities);
|
string VaapiCapabilities);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public static class FileSystemLayout
|
|||||||
public static readonly string AppDataFolder;
|
public static readonly string AppDataFolder;
|
||||||
|
|
||||||
public static readonly string TranscodeFolder;
|
public static readonly string TranscodeFolder;
|
||||||
|
public static readonly string TranscodeTroubleshootingFolder;
|
||||||
|
|
||||||
public static readonly string DataProtectionFolder;
|
public static readonly string DataProtectionFolder;
|
||||||
public static readonly string LogsFolder;
|
public static readonly string LogsFolder;
|
||||||
@@ -120,6 +121,7 @@ public static class FileSystemLayout
|
|||||||
}
|
}
|
||||||
|
|
||||||
TranscodeFolder = useCustomTranscodeFolder ? customTranscodeFolder : defaultTranscodeFolder;
|
TranscodeFolder = useCustomTranscodeFolder ? customTranscodeFolder : defaultTranscodeFolder;
|
||||||
|
TranscodeTroubleshootingFolder = Path.Combine(TranscodeFolder, ".troubleshooting");
|
||||||
|
|
||||||
DataProtectionFolder = Path.Combine(AppDataFolder, "data-protection");
|
DataProtectionFolder = Path.Combine(AppDataFolder, "data-protection");
|
||||||
LogsFolder = Path.Combine(AppDataFolder, "logs");
|
LogsFolder = Path.Combine(AppDataFolder, "logs");
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Notifications;
|
||||||
|
|
||||||
|
public record PlaybackTroubleshootingCompletedNotification(int ExitCode) : INotification;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using CliWrap;
|
using CliWrap;
|
||||||
using ErsatzTV.Application;
|
using ErsatzTV.Application;
|
||||||
|
using ErsatzTV.Application.MediaItems;
|
||||||
using ErsatzTV.Application.Troubleshooting;
|
using ErsatzTV.Application.Troubleshooting;
|
||||||
|
using ErsatzTV.Application.Troubleshooting.Queries;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Interfaces.Locking;
|
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -12,7 +13,6 @@ namespace ErsatzTV.Controllers.Api;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class TroubleshootController(
|
public class TroubleshootController(
|
||||||
IEntityLocker entityLocker,
|
|
||||||
ChannelWriter<IFFmpegWorkerRequest> channelWriter,
|
ChannelWriter<IFFmpegWorkerRequest> channelWriter,
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
IMediator mediator) : ControllerBase
|
IMediator mediator) : ControllerBase
|
||||||
@@ -28,8 +28,6 @@ public class TroubleshootController(
|
|||||||
int watermark,
|
int watermark,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
entityLocker.LockTroubleshootingPlayback();
|
|
||||||
|
|
||||||
Either<BaseError, Command> result = await mediator.Send(
|
Either<BaseError, Command> result = await mediator.Send(
|
||||||
new PrepareTroubleshootingPlayback(mediaItem, ffmpegProfile, watermark),
|
new PrepareTroubleshootingPlayback(mediaItem, ffmpegProfile, watermark),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
@@ -37,21 +35,41 @@ public class TroubleshootController(
|
|||||||
return await result.MatchAsync<IActionResult>(
|
return await result.MatchAsync<IActionResult>(
|
||||||
async command =>
|
async command =>
|
||||||
{
|
{
|
||||||
await channelWriter.WriteAsync(new StartTroubleshootingPlayback(command), CancellationToken.None);
|
Either<BaseError, MediaItemInfo> maybeMediaInfo = await mediator.Send(new GetMediaItemInfo(mediaItem), cancellationToken);
|
||||||
string playlistFile = Path.Combine(FileSystemLayout.TranscodeFolder, ".troubleshooting", "live.m3u8");
|
foreach (MediaItemInfo mediaInfo in maybeMediaInfo.RightToSeq())
|
||||||
|
|
||||||
DateTimeOffset start = DateTimeOffset.Now;
|
|
||||||
while (!localFileSystem.FileExists(playlistFile) &&
|
|
||||||
DateTimeOffset.Now - start < TimeSpan.FromSeconds(15))
|
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken);
|
TroubleshootingInfo troubleshootingInfo = await mediator.Send(
|
||||||
if (cancellationToken.IsCancellationRequested)
|
new GetTroubleshootingInfo(),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
// filter ffmpeg profiles
|
||||||
|
troubleshootingInfo.FFmpegProfiles.RemoveAll(p => p.Id != ffmpegProfile);
|
||||||
|
|
||||||
|
// filter watermarks
|
||||||
|
troubleshootingInfo.Watermarks.RemoveAll(p => p.Id != watermark);
|
||||||
|
|
||||||
|
await channelWriter.WriteAsync(
|
||||||
|
new StartTroubleshootingPlayback(command, mediaInfo, troubleshootingInfo),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
string playlistFile = Path.Combine(
|
||||||
|
FileSystemLayout.TranscodeFolder,
|
||||||
|
".troubleshooting",
|
||||||
|
"live.m3u8");
|
||||||
|
|
||||||
|
while (!localFileSystem.FileExists(playlistFile))
|
||||||
{
|
{
|
||||||
break;
|
await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken);
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Redirect("~/iptv/session/.troubleshooting/live.m3u8");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Redirect("~/iptv/session/.troubleshooting/live.m3u8");
|
return NotFound();
|
||||||
},
|
},
|
||||||
_ => NotFound());
|
_ => NotFound());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
@using ErsatzTV.Application.FFmpegProfiles
|
@using ErsatzTV.Application.FFmpegProfiles
|
||||||
@using ErsatzTV.Application.MediaItems
|
@using ErsatzTV.Application.MediaItems
|
||||||
@using ErsatzTV.Application.Watermarks
|
@using ErsatzTV.Application.Watermarks
|
||||||
|
@using ErsatzTV.Core.Notifications
|
||||||
|
@using MediatR.Courier
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@inject IMediator Mediator
|
@inject IMediator Mediator
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject IJSRuntime JsRuntime
|
@inject IJSRuntime JsRuntime
|
||||||
@inject IEntityLocker Locker
|
@inject IEntityLocker Locker
|
||||||
|
@inject ICourier Courier;
|
||||||
|
@inject ISnackbar Snackbar;
|
||||||
|
|
||||||
<MudForm Style="max-height: 100%">
|
<MudForm Style="max-height: 100%">
|
||||||
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
|
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
|
||||||
@@ -109,7 +113,11 @@
|
|||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnInitialized() => Locker.OnTroubleshootingPlaybackChanged += LockChanged;
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Locker.OnTroubleshootingPlaybackChanged += LockChanged;
|
||||||
|
Courier.Subscribe<PlaybackTroubleshootingCompletedNotification>(HandleTroubleshootingCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
@@ -126,13 +134,14 @@
|
|||||||
|
|
||||||
private async Task PreviewChannel()
|
private async Task PreviewChannel()
|
||||||
{
|
{
|
||||||
Locker.LockTroubleshootingPlayback();
|
|
||||||
_hasPlayed = true;
|
|
||||||
|
|
||||||
var uri = new UriBuilder(NavigationManager.ToAbsoluteUri(NavigationManager.Uri));
|
var uri = new UriBuilder(NavigationManager.ToAbsoluteUri(NavigationManager.Uri));
|
||||||
uri.Path = uri.Path.Replace("/system/troubleshooting/playback", "/api/troubleshoot/playback.m3u8");
|
uri.Path = uri.Path.Replace("/system/troubleshooting/playback", "/api/troubleshoot/playback.m3u8");
|
||||||
uri.Query = $"?mediaItem={_mediaItemId}&ffmpegProfile={_ffmpegProfileId}&watermark={_watermarkId ?? 0}";
|
uri.Query = $"?mediaItem={_mediaItemId}&ffmpegProfile={_ffmpegProfileId}&watermark={_watermarkId ?? 0}";
|
||||||
await JsRuntime.InvokeVoidAsync("previewChannel", uri.ToString());
|
await JsRuntime.InvokeVoidAsync("previewChannel", uri.ToString());
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
_hasPlayed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnMediaItemIdChanged(int? mediaItemId)
|
private async Task OnMediaItemIdChanged(int? mediaItemId)
|
||||||
@@ -162,4 +171,16 @@
|
|||||||
await JsRuntime.InvokeVoidAsync("window.open", $"api/troubleshoot/playback/archive?mediaItem={_mediaItemId ?? 0}&ffmpegProfile={_ffmpegProfileId}&watermark={_watermarkId ?? 0}");
|
await JsRuntime.InvokeVoidAsync("window.open", $"api/troubleshoot/playback/archive?mediaItem={_mediaItemId ?? 0}&ffmpegProfile={_ffmpegProfileId}&watermark={_watermarkId ?? 0}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleTroubleshootingCompleted(PlaybackTroubleshootingCompletedNotification result)
|
||||||
|
{
|
||||||
|
if (result.ExitCode == 0)
|
||||||
|
{
|
||||||
|
Snackbar.Add("FFmpeg troubleshooting process exited successfully", Severity.Success);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Snackbar.Add($"FFmpeg troubleshooting process exited with code {result.ExitCode}", Severity.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -58,6 +58,14 @@
|
|||||||
if (Hls.isSupported()) {
|
if (Hls.isSupported()) {
|
||||||
var hls = new Hls({
|
var hls = new Hls({
|
||||||
debug: true,
|
debug: true,
|
||||||
|
manifestLoadPolicy: {
|
||||||
|
default: {
|
||||||
|
maxTimeToFirstByteMs: Infinity,
|
||||||
|
maxLoadTimeMs: 60000,
|
||||||
|
timeoutRetry: null,
|
||||||
|
errorRetry: null
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
$('#video').data('hls', hls);
|
$('#video').data('hls', hls);
|
||||||
hls.loadSource(uri);
|
hls.loadSource(uri);
|
||||||
|
|||||||
Reference in New Issue
Block a user