1+ using Jellyfin . Data . Enums ;
2+ using MediaBrowser . Common . Plugins ;
3+ using MediaBrowser . Controller . Entities ;
4+ using MediaBrowser . Controller . Library ;
5+ using MediaBrowser . Controller . MediaEncoding ;
6+ using MediaBrowser . Controller . Persistence ;
7+ using MediaBrowser . Controller . Streaming ;
8+ using MediaBrowser . Model . Tasks ;
9+
10+ public class RemuxLibraryTask ( IItemRepository _itemRepo ,
11+ IMediaSourceManager _sourceManager ,
12+ ITranscodeManager _transcodeManager ,
13+ IPluginManager _pluginManager )
14+ : IScheduledTask
15+ {
16+ public string Name => "Remux Dolby Vision MKVs" ;
17+
18+ public string Key => nameof ( RemuxLibraryTask ) ;
19+
20+ public string Description => "" ;
21+
22+ public string Category => "Library" ;
23+
24+ public IEnumerable < TaskTriggerInfo > GetDefaultTriggers ( )
25+ {
26+ return [ new TaskTriggerInfo ( ) { Type = TaskTriggerInfo . TriggerDaily , TimeOfDayTicks = 0 } ] ;
27+ }
28+
29+ public async Task ExecuteAsync ( IProgress < double > progress , CancellationToken cancellationToken )
30+ {
31+ var plugin = _pluginManager . GetPlugin ( DoViRemuxPlugin . OurGuid ) ? . Instance as DoViRemuxPlugin
32+ ?? throw new Exception ( "Can't get DoViRemuxPlugin instance" ) ;
33+
34+ var configuration = plugin . Configuration ;
35+
36+ var allItems = _itemRepo . GetItems ( new InternalItemsQuery
37+ {
38+ MediaTypes = [ MediaType . Video ] ,
39+ AncestorIds = configuration . OnlyRemuxLibraries ? . Split ( "," ) . Select ( Guid . Parse ) . ToArray ( )
40+ ?? [ ]
41+ } ) ;
42+
43+ foreach ( var item in allItems . Items )
44+ {
45+ await ProcessOneItem ( item , cancellationToken ) ;
46+ }
47+ }
48+
49+ private async Task ProcessOneItem ( BaseItem item , CancellationToken cancellationToken )
50+ {
51+ if ( item . Container != "mkv" ) return ;
52+
53+ var streams = _sourceManager . GetMediaStreams ( item . Id ) ;
54+ var doviStream = streams . FirstOrDefault ( s => s . Type == MediaBrowser . Model . Entities . MediaStreamType . Video
55+ && s . DvProfile . HasValue ) ;
56+
57+ if ( doviStream is not { DvProfile : 8 , DvVersionMajor : 1 } ) return ;
58+
59+ // if there's an existing MP4 source, assume we made it.
60+ // also I can't decide if I like that the model object comes back with services inside it
61+ // which can run lookups like this. The API is sort of clean, actually, but... am I just a hater?
62+ var otherSources = item . GetMediaSources ( true ) ;
63+ if ( otherSources . Any ( s => s . Container == "mp4" ) ) return ;
64+
65+ var ourSource = otherSources . First ( s => s . Container == "mkv" ) ;
66+
67+ var inputPath = ourSource . Path ;
68+ var outputPath = $ "{ inputPath } .mp4";
69+
70+ var remuxRequest = new StreamState ( _sourceManager , TranscodingJobType . Progressive , _transcodeManager ) ;
71+
72+ remuxRequest . MediaSource = ourSource ;
73+ remuxRequest . Request = new StreamingRequestDto
74+ {
75+ LiveStreamId = null // i don't remember why this has to be null
76+ } ;
77+
78+ remuxRequest . MediaPath = outputPath ;
79+
80+ // avoids NREs and changes the log filename prefix from "Transcode" to "Remux"
81+ remuxRequest . OutputContainer = "mp4" ;
82+ remuxRequest . OutputAudioCodec = "copy" ;
83+ remuxRequest . OutputVideoCodec = "copy" ;
84+
85+ string cli = "-analyzeduration 200M -probesize 1G -fflags +genpts " ;
86+ cli += $ "-i \" { inputPath } \" ";
87+ cli += "-map_metadata -1 -map_chapters -1 -threads 0 -map 0:0 -map 0:1 -map -0:s " ;
88+ cli += "-codec:v:0 copy -tag:v:0 dvh1 -strict -2 -bsf:v hevc_mp4toannexb -start_at_zero " ;
89+ cli += "-codec:a:0 copy -copyts -avoid_negative_ts disabled -max_muxing_queue_size 2048 " ;
90+ cli += $ "\" { outputPath } \" ";
91+
92+ var remuxCancelToken = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
93+ var job = await _transcodeManager . StartFfMpeg ( remuxRequest , outputPath , cli , Guid . Empty , TranscodingJobType . Progressive , remuxCancelToken ) ;
94+ }
95+ }
0 commit comments