Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Jellyfin.Plugin.DoViRemux/Jellyfin.Plugin.DoViRemux.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="10.10.6">
<PackageReference Include="Jellyfin.Controller" Version="10.11.3">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
Expand Down
120 changes: 72 additions & 48 deletions Jellyfin.Plugin.DoViRemux/RemuxLibraryTask.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
using System.Linq;

using Jellyfin.Data.Entities;

Check failure on line 4 in Jellyfin.Plugin.DoViRemux/RemuxLibraryTask.cs

View workflow job for this annotation

GitHub Actions / call / build

The type or namespace name 'Entities' does not exist in the namespace 'Jellyfin.Data' (are you missing an assembly reference?)
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
Expand All @@ -13,16 +16,17 @@

namespace Jellyfin.Plugin.DoViRemux;

public class RemuxLibraryTask(IItemRepository _itemRepo,
IMediaSourceManager _sourceManager,
ITranscodeManager _transcodeManager,
IPluginManager _pluginManager,
ILogger<RemuxLibraryTask> _logger,
IApplicationPaths _paths,
ILibraryManager _libraryManager,
IUserDataManager _userDataManager,
IUserManager _userManager,
DownmuxWorkflow _downmuxWorkflow)
public class RemuxLibraryTask(
IItemRepository _itemRepo,
IMediaSourceManager _sourceManager,
ITranscodeManager _transcodeManager,
IPluginManager _pluginManager,
ILogger<RemuxLibraryTask> _logger,
IApplicationPaths _paths,
ILibraryManager _libraryManager,
IUserDataManager _userDataManager,
IUserManager _userManager,
DownmuxWorkflow _downmuxWorkflow)
: IScheduledTask
{
public string Name => "Remux Dolby Vision MKVs";
Expand All @@ -40,10 +44,25 @@
var configuration = (_pluginManager.GetPlugin(Plugin.OurGuid)?.Instance as Plugin)?.Configuration
?? throw new Exception("Can't get plugin configuration");

var primaryUser = !string.IsNullOrEmpty(configuration.PrimaryUser)
? _userManager.GetUserByName(configuration.PrimaryUser)
?? throw new Exception($"Primary user '{configuration.PrimaryUser}' does not exist")
: null;
// Jellyfin 10.11+: avoid calling IUserManager.GetUserByName(string) which no longer exists.
// Instead, resolve the primary user by name from the Users collection.
User? primaryUser = null;

if (!string.IsNullOrWhiteSpace(configuration.PrimaryUser))
{
var primaryUserName = configuration.PrimaryUser.Trim();

primaryUser = _userManager.Users
.FirstOrDefault(u =>
u is not null &&
!string.IsNullOrEmpty(u.Name) &&
string.Equals(u.Name, primaryUserName, StringComparison.OrdinalIgnoreCase));

if (primaryUser is null)
{
throw new Exception($"Primary user '{configuration.PrimaryUser}' does not exist");
}
}

var itemsToProcess = _itemRepo.GetItems(new InternalItemsQuery
{
Expand Down Expand Up @@ -78,7 +97,7 @@
}
}

private bool ShouldProcessItem(Video item, User? primaryUser)

Check failure on line 100 in Jellyfin.Plugin.DoViRemux/RemuxLibraryTask.cs

View workflow job for this annotation

GitHub Actions / call / build

The type or namespace name 'User' could not be found (are you missing a using directive or an assembly reference?)
{
if (item.Container != "mkv") return false;

Expand All @@ -100,13 +119,11 @@
// like trickplay/thumbnail images (or even the entire video) may show up as the silly purple-and-green versions.
if (doviStream?.DvProfile is null) return false;

// if there's an existing MP4 source, assume we made it.
// also I can't decide if I like that the model object comes back with services inside it
// which can run lookups like this. The API is sort of clean, actually, but... am I just a hater?
// If there's an existing MP4 source, assume we made it already.
var otherSources = item.GetMediaSources(true);
if (otherSources.Any(s => s.Container == "mp4")) return false;

// or if there's an unmerged, standalone item at the expected path
// Or if there's an unmerged, standalone item at the expected path
if (_libraryManager.FindByPath(item.Path + ".mp4", false) is not null) return false;

return true;
Expand All @@ -118,12 +135,12 @@
var otherSources = item.GetMediaSources(true);
var ourSource = otherSources.First(s => s.Container == "mkv");

// we remux to a temporary file first, then move it to the final directory.
// this improves performance when jellyfin's temp directory is on a separate
// We remux to a temporary file first, then move it to the final directory.
// This improves performance when Jellyfin's temp directory is on a separate
// drive from the original media, because there's no simultaneous IO. It also
// avoids the problem of jellyfin trying to process the file before it's done,
// avoids the problem of Jellyfin trying to process the file before it's done,
// which can impact things like trickplay (or anything that uses ffprobe,
// though the faststart flag can help with that)
// though the faststart flag can help with that).
var inputPath = ourSource.Path;
var finalPath = $"{inputPath}.mp4";
var outputPath = Path.Combine(_paths.TempDirectory, finalPath.GetHashCode() + ".mp4");
Expand All @@ -139,7 +156,7 @@
File.Delete(outputPath);
}

// profile 7.6 can be converted to profile 8.1, with some special handling.
// Profile 7.6 can be converted to profile 8.1 with some special handling.
// This will be presented as a second input to FFmpeg, and it will copy that
// converted video stream instead of our original MKV's.
string? downmuxedVideoPath = null;
Expand All @@ -149,21 +166,21 @@
downmuxedVideoPath = await _downmuxWorkflow.Downmux(ourSource, cancellationToken);
}

// truehd isn't supported by many consumer MP4 decoders even though ffmpeg can do it.
// it's found on a lot of DoVi media (cough particularly hybrid remuxes cough),
// but media like Bluray is required to have an AAC/AC3/whatever fallback stream for compatibility
// TrueHD isn't supported by many consumer MP4 decoders even though FFmpeg can handle it.
// It's found on a lot of DoVi media (particularly hybrid remuxes),
// but media like Blu-ray is required to have an AAC/AC3/whatever fallback stream for compatibility.
var audioStreams = streams.Where(s => s.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio
&& s.Codec != "truehd")
.Select((audioStream, i) => new { audioStream.Index, OutputIndex = i, audioStream.Language})
.Select((audioStream, i) => new { audioStream.Index, OutputIndex = i, audioStream.Language })
.ToList();
if (audioStreams.Count == 0)
{
// TODO: transcode it instead
throw new Exception("Couldn't find an appropriate audio stream to copy");
}

// PGS subtitles aren't supported by mp4. Technically we can use the copy codec
// and most decoders will know how to use subrip subtitles, but mov_text is standard
// PGS subtitles aren't supported by MP4. Technically we can use the copy codec
// and most decoders will know how to use SubRip subtitles, but mov_text is standard.
// We also need to exclude external subs, which show up as a stream even though they're
// not in the video file itself, so FFmpeg will be confused if we mention them to it.
var subtitles = streams
Expand All @@ -173,30 +190,31 @@
.Select((subtitle, i) => new { subtitle.Index, OutputIndex = i, Codec = "mov_text", Lang = subtitle.Language })
.ToList();

var remuxRequest = new StreamState(_sourceManager, TranscodingJobType.Progressive, _transcodeManager);

remuxRequest.MediaSource = ourSource;
remuxRequest.Request = new StreamingRequestDto
var remuxRequest = new StreamState(_sourceManager, TranscodingJobType.Progressive, _transcodeManager)
{
LiveStreamId = null // i don't remember why this has to be null
MediaSource = ourSource,
Request = new StreamingRequestDto
{
// Must be null for this usage
LiveStreamId = null
},
MediaPath = outputPath,

// Avoid NREs and change the log filename prefix from "Transcode" to "Remux"
OutputContainer = "mp4",
OutputAudioCodec = "copy",
OutputVideoCodec = "copy"
};

remuxRequest.MediaPath = outputPath;

// avoids NREs and changes the log filename prefix from "Transcode" to "Remux"
remuxRequest.OutputContainer = "mp4";
remuxRequest.OutputAudioCodec = "copy";
remuxRequest.OutputVideoCodec = "copy";

string cli = "-analyzeduration 200M -probesize 1G -fflags +genpts ";

cli += $"-i \"{inputPath}\" ";
if (downmuxedVideoPath is not null)
{
cli += $"-i \"{downmuxedVideoPath}\" ";
}
cli += $"-map_metadata -1 -map_chapters -1 -threads 0 ";

cli += "-map_metadata -1 -map_chapters -1 -threads 0 ";

if (downmuxedVideoPath is not null)
{
Expand All @@ -213,7 +231,7 @@
// This doesn't need to change if we're following the profile 7 path:
// the bitstream filter will have no effect if it's already Annex B,
// we still need to tag it with the correct codec, and at this point
// we're referencing the output stream index (0) instead of the input
// we're referencing the output stream index (0) instead of the input.
cli += "-codec:v:0 copy -tag:v:0 dvh1 -strict experimental -bsf:v hevc_mp4toannexb -start_at_zero ";

cli += string.Concat(audioStreams.Select(a => $"-codec:a:{a.OutputIndex} copy "));
Expand All @@ -226,8 +244,14 @@
cancellationToken.ThrowIfCancellationRequested();

var remuxCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
using var job = await _transcodeManager.StartFfMpeg(remuxRequest, outputPath, cli, Guid.Empty, TranscodingJobType.Progressive, remuxCancelToken);

using var job = await _transcodeManager.StartFfMpeg(
remuxRequest,
outputPath,
cli,
Guid.Empty,
TranscodingJobType.Progressive,
remuxCancelToken);

while (!cancellationToken.IsCancellationRequested && !job.HasExited)
{
await Task.Delay(1000, cancellationToken);
Expand All @@ -240,11 +264,11 @@
finally
{
File.Delete(outputPath);

if (downmuxedVideoPath is not null)
{
File.Delete(downmuxedVideoPath);
}
}
}
}
}
11 changes: 3 additions & 8 deletions meta.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: "DoViRemux"
guid: "2f215b63-1a73-4193-9102-78f84d027014"
version: 3.2.0
targetAbi: "10.10.6.0"
version: 3.2.1
targetAbi: "10.11.0.0"
framework: "net8.0"
owner: "katiekloss"
overview: "Automatically remux Dolby Vision files"
Expand All @@ -12,9 +12,4 @@ category: "General"
artifacts:
- "Jellyfin.Plugin.DoViRemux.dll"
changelog: |-
- Fixed an error on task start when no primary user is set
- Fixed remuxes crashing when the media has external subtitles
- Fixed configuration changes requiring a restart to take effect
- Improved logging for the 7.6 to 8.1 downmux processes
- Fixed a deadlock between ffmpeg and dovi_tool during downmuxes
- 7.6 downmuxing is no longer enabled by default after initial plugin installation
- Updated Jellyfin dependency compatibility to 10.11.x
Loading