2023-02-22 08:08:35 +00:00
using System ;
using System.Collections.Generic ;
using System.Globalization ;
using System.IO ;
using System.Linq ;
2023-06-23 21:22:00 +00:00
using System.Text ;
2023-02-22 08:08:35 +00:00
using System.Threading ;
using System.Threading.Tasks ;
2024-01-14 11:11:16 +00:00
using AsyncKeyedLock ;
2023-06-27 00:40:10 +00:00
using Jellyfin.Data.Entities ;
2023-07-01 23:16:41 +00:00
using MediaBrowser.Common.Configuration ;
2023-02-25 23:59:46 +00:00
using MediaBrowser.Controller.Configuration ;
2023-05-30 21:23:02 +00:00
using MediaBrowser.Controller.Drawing ;
2023-02-22 08:08:35 +00:00
using MediaBrowser.Controller.Entities ;
2023-02-24 00:04:35 +00:00
using MediaBrowser.Controller.Library ;
2023-02-22 08:08:35 +00:00
using MediaBrowser.Controller.MediaEncoding ;
using MediaBrowser.Controller.Trickplay ;
2023-02-25 23:59:46 +00:00
using MediaBrowser.Model.Configuration ;
2023-02-22 08:08:35 +00:00
using MediaBrowser.Model.Entities ;
using MediaBrowser.Model.IO ;
2023-06-27 00:40:10 +00:00
using Microsoft.EntityFrameworkCore ;
2023-02-22 08:08:35 +00:00
using Microsoft.Extensions.Logging ;
2023-06-27 00:40:10 +00:00
namespace Jellyfin.Server.Implementations.Trickplay ;
2023-05-01 19:51:05 +00:00
/// <summary>
/// ITrickplayManager implementation.
/// </summary>
public class TrickplayManager : ITrickplayManager
2023-02-22 08:08:35 +00:00
{
2023-05-01 19:51:05 +00:00
private readonly ILogger < TrickplayManager > _logger ;
private readonly IMediaEncoder _mediaEncoder ;
private readonly IFileSystem _fileSystem ;
private readonly EncodingHelper _encodingHelper ;
private readonly ILibraryManager _libraryManager ;
private readonly IServerConfigurationManager _config ;
2023-05-30 21:23:02 +00:00
private readonly IImageEncoder _imageEncoder ;
2023-06-27 00:40:10 +00:00
private readonly IDbContextFactory < JellyfinDbContext > _dbProvider ;
2023-07-01 23:16:41 +00:00
private readonly IApplicationPaths _appPaths ;
2023-05-01 19:51:05 +00:00
2024-01-14 11:11:16 +00:00
private static readonly AsyncNonKeyedLocker _resourcePool = new ( 1 ) ;
2023-05-18 06:25:52 +00:00
private static readonly string [ ] _trickplayImgExtensions = { ".jpg" } ;
2023-05-01 19:51:05 +00:00
2023-02-22 08:08:35 +00:00
/// <summary>
2023-05-01 19:51:05 +00:00
/// Initializes a new instance of the <see cref="TrickplayManager"/> class.
2023-02-22 08:08:35 +00:00
/// </summary>
2023-05-01 19:51:05 +00:00
/// <param name="logger">The logger.</param>
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="fileSystem">The file systen.</param>
/// <param name="encodingHelper">The encoding helper.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="config">The server configuration manager.</param>
2023-05-30 21:23:02 +00:00
/// <param name="imageEncoder">The image encoder.</param>
2023-06-27 00:40:10 +00:00
/// <param name="dbProvider">The database provider.</param>
2023-07-01 23:16:41 +00:00
/// <param name="appPaths">The application paths.</param>
2023-05-01 19:51:05 +00:00
public TrickplayManager (
ILogger < TrickplayManager > logger ,
IMediaEncoder mediaEncoder ,
IFileSystem fileSystem ,
EncodingHelper encodingHelper ,
ILibraryManager libraryManager ,
2023-05-30 21:23:02 +00:00
IServerConfigurationManager config ,
2023-06-27 00:40:10 +00:00
IImageEncoder imageEncoder ,
2023-07-01 23:16:41 +00:00
IDbContextFactory < JellyfinDbContext > dbProvider ,
IApplicationPaths appPaths )
2023-02-22 08:08:35 +00:00
{
2023-05-01 19:51:05 +00:00
_logger = logger ;
_mediaEncoder = mediaEncoder ;
_fileSystem = fileSystem ;
_encodingHelper = encodingHelper ;
_libraryManager = libraryManager ;
_config = config ;
2023-05-30 21:23:02 +00:00
_imageEncoder = imageEncoder ;
2023-06-27 00:40:10 +00:00
_dbProvider = dbProvider ;
2023-07-01 23:16:41 +00:00
_appPaths = appPaths ;
2023-05-01 19:51:05 +00:00
}
/// <inheritdoc />
2024-09-07 17:23:48 +00:00
public async Task MoveGeneratedTrickplayDataAsync ( Video video , LibraryOptions ? libraryOptions , CancellationToken cancellationToken )
{
var options = _config . Configuration . TrickplayOptions ;
if ( ! CanGenerateTrickplay ( video , options . Interval ) )
{
return ;
}
var existingTrickplayResolutions = await GetTrickplayResolutions ( video . Id ) . ConfigureAwait ( false ) ;
foreach ( var resolution in existingTrickplayResolutions )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
var existingResolution = resolution . Key ;
var tileWidth = resolution . Value . TileWidth ;
var tileHeight = resolution . Value . TileHeight ;
var shouldBeSavedWithMedia = libraryOptions is null ? false : libraryOptions . SaveTrickplayWithMedia ;
var localOutputDir = GetTrickplayDirectory ( video , tileWidth , tileHeight , existingResolution , false ) ;
var mediaOutputDir = GetTrickplayDirectory ( video , tileWidth , tileHeight , existingResolution , true ) ;
if ( shouldBeSavedWithMedia & & Directory . Exists ( localOutputDir ) )
{
var localDirFiles = Directory . GetFiles ( localOutputDir ) ;
var mediaDirExists = Directory . Exists ( mediaOutputDir ) ;
if ( localDirFiles . Length > 0 & & ( ( mediaDirExists & & Directory . GetFiles ( mediaOutputDir ) . Length = = 0 ) | | ! mediaDirExists ) )
{
// Move images from local dir to media dir
MoveContent ( localOutputDir , mediaOutputDir ) ;
_logger . LogInformation ( "Moved trickplay images for {ItemName} to {Location}" , video . Name , mediaOutputDir ) ;
}
}
else if ( Directory . Exists ( mediaOutputDir ) )
{
var mediaDirFiles = Directory . GetFiles ( mediaOutputDir ) ;
var localDirExists = Directory . Exists ( localOutputDir ) ;
if ( mediaDirFiles . Length > 0 & & ( ( localDirExists & & Directory . GetFiles ( localOutputDir ) . Length = = 0 ) | | ! localDirExists ) )
{
// Move images from media dir to local dir
MoveContent ( mediaOutputDir , localOutputDir ) ;
_logger . LogInformation ( "Moved trickplay images for {ItemName} to {Location}" , video . Name , localOutputDir ) ;
}
}
}
}
private void MoveContent ( string sourceFolder , string destinationFolder )
{
_fileSystem . MoveDirectory ( sourceFolder , destinationFolder ) ;
var parent = Directory . GetParent ( sourceFolder ) ;
if ( parent is not null )
{
var parentContent = Directory . GetDirectories ( parent . FullName ) ;
if ( parentContent . Length = = 0 )
{
Directory . Delete ( parent . FullName ) ;
}
}
}
/// <inheritdoc />
public async Task RefreshTrickplayDataAsync ( Video video , bool replace , LibraryOptions ? libraryOptions , CancellationToken cancellationToken )
2023-05-01 19:51:05 +00:00
{
_logger . LogDebug ( "Trickplay refresh for {ItemId} (replace existing: {Replace})" , video . Id , replace ) ;
var options = _config . Configuration . TrickplayOptions ;
2024-05-25 15:46:18 +00:00
if ( options . Interval < 1000 )
{
_logger . LogWarning ( "Trickplay image interval {Interval} is too small, reset to the minimum valid value of 1000" , options . Interval ) ;
options . Interval = 1000 ;
}
2023-05-01 19:51:05 +00:00
foreach ( var width in options . WidthResolutions )
2023-02-22 08:08:35 +00:00
{
2023-05-01 19:51:05 +00:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
await RefreshTrickplayDataInternal (
video ,
replace ,
width ,
options ,
2024-09-07 17:23:48 +00:00
libraryOptions ,
2023-05-01 19:51:05 +00:00
cancellationToken ) . ConfigureAwait ( false ) ;
2023-02-22 08:08:35 +00:00
}
2023-05-01 19:51:05 +00:00
}
2023-02-22 08:08:35 +00:00
2023-05-01 19:51:05 +00:00
private async Task RefreshTrickplayDataInternal (
Video video ,
bool replace ,
int width ,
TrickplayOptions options ,
2024-09-07 17:23:48 +00:00
LibraryOptions ? libraryOptions ,
2023-05-01 19:51:05 +00:00
CancellationToken cancellationToken )
{
if ( ! CanGenerateTrickplay ( video , options . Interval ) )
2023-02-22 08:08:35 +00:00
{
2023-05-01 19:51:05 +00:00
return ;
2023-02-22 08:08:35 +00:00
}
2023-05-01 19:51:05 +00:00
var imgTempDir = string . Empty ;
2024-01-14 11:11:16 +00:00
using ( await _resourcePool . LockAsync ( cancellationToken ) . ConfigureAwait ( false ) )
2023-02-22 08:08:35 +00:00
{
2024-01-14 11:11:16 +00:00
try
2023-02-22 08:08:35 +00:00
{
2024-01-14 11:11:16 +00:00
// Extract images
// Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
var mediaSource = video . GetMediaSources ( false ) . Find ( source = > Guid . Parse ( source . Id ) . Equals ( video . Id ) ) ;
2023-05-01 19:51:05 +00:00
2024-01-14 11:11:16 +00:00
if ( mediaSource is null )
{
_logger . LogDebug ( "Found no matching media source for item {ItemId}" , video . Id ) ;
return ;
}
2023-05-01 19:51:05 +00:00
2024-05-17 17:51:45 +00:00
var mediaPath = mediaSource . Path ;
if ( ! File . Exists ( mediaPath ) )
{
2024-05-18 11:13:34 +00:00
_logger . LogWarning ( "Media not found at {Path} for item {ItemID}" , mediaPath , video . Id ) ;
2024-05-17 17:51:45 +00:00
return ;
}
2024-04-30 11:41:46 +00:00
// The width has to be even, otherwise a lot of filters will not be able to sample it
var actualWidth = 2 * ( width / 2 ) ;
// Force using the video width when the trickplay setting has a too large width
if ( mediaSource . VideoStream . Width is not null & & mediaSource . VideoStream . Width < width )
{
_logger . LogWarning ( "Video width {VideoWidth} is smaller than trickplay setting {TrickPlayWidth}, using video width for thumbnails" , mediaSource . VideoStream . Width , width ) ;
actualWidth = 2 * ( ( int ) mediaSource . VideoStream . Width / 2 ) ;
}
2024-09-07 17:23:48 +00:00
var tileWidth = options . TileWidth ;
var tileHeight = options . TileHeight ;
var saveWithMedia = libraryOptions is null ? false : libraryOptions . SaveTrickplayWithMedia ;
var outputDir = GetTrickplayDirectory ( video , tileWidth , tileHeight , actualWidth , saveWithMedia ) ;
2024-04-30 11:41:46 +00:00
2024-09-07 17:23:48 +00:00
// Import existing trickplay tiles
if ( ! replace & & Directory . Exists ( outputDir ) )
2024-04-30 11:41:46 +00:00
{
2024-09-07 17:23:48 +00:00
var existingFiles = Directory . GetFiles ( outputDir ) ;
if ( existingFiles . Length > 0 )
{
var hasTrickplayResolution = await HasTrickplayResolutionAsync ( video . Id , actualWidth ) . ConfigureAwait ( false ) ;
if ( hasTrickplayResolution )
{
_logger . LogDebug ( "Found existing trickplay files for {ItemId}." , video . Id ) ;
return ;
}
// Import tiles
var localTrickplayInfo = new TrickplayInfo
{
ItemId = video . Id ,
Width = width ,
Interval = options . Interval ,
TileWidth = options . TileWidth ,
TileHeight = options . TileHeight ,
ThumbnailCount = existingFiles . Length ,
Height = 0 ,
Bandwidth = 0
} ;
foreach ( var tile in existingFiles )
{
var image = _imageEncoder . GetImageSize ( tile ) ;
localTrickplayInfo . Height = Math . Max ( localTrickplayInfo . Height , image . Height ) ;
var bitrate = ( int ) Math . Ceiling ( ( decimal ) new FileInfo ( tile ) . Length * 8 / localTrickplayInfo . TileWidth / localTrickplayInfo . TileHeight / ( localTrickplayInfo . Interval / 1000 ) ) ;
localTrickplayInfo . Bandwidth = Math . Max ( localTrickplayInfo . Bandwidth , bitrate ) ;
}
await SaveTrickplayInfo ( localTrickplayInfo ) . ConfigureAwait ( false ) ;
_logger . LogDebug ( "Imported existing trickplay files for {ItemId}." , video . Id ) ;
return ;
}
2024-04-30 11:41:46 +00:00
}
2024-09-07 17:23:48 +00:00
// Generate trickplay tiles
2024-01-14 11:11:16 +00:00
var mediaStream = mediaSource . VideoStream ;
var container = mediaSource . Container ;
2024-04-30 11:41:46 +00:00
_logger . LogInformation ( "Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]" , actualWidth , mediaPath , video . Id ) ;
2024-01-14 11:11:16 +00:00
imgTempDir = await _mediaEncoder . ExtractVideoImagesOnIntervalAccelerated (
mediaPath ,
container ,
mediaSource ,
mediaStream ,
2024-04-30 11:41:46 +00:00
actualWidth ,
2024-01-14 11:11:16 +00:00
TimeSpan . FromMilliseconds ( options . Interval ) ,
options . EnableHwAcceleration ,
2024-03-28 15:26:01 +00:00
options . EnableHwEncoding ,
2024-01-14 11:11:16 +00:00
options . ProcessThreads ,
options . Qscale ,
options . ProcessPriority ,
2024-05-08 05:50:03 +00:00
options . EnableKeyFrameOnlyExtraction ,
2024-01-14 11:11:16 +00:00
_encodingHelper ,
cancellationToken ) . ConfigureAwait ( false ) ;
if ( string . IsNullOrEmpty ( imgTempDir ) | | ! Directory . Exists ( imgTempDir ) )
{
throw new InvalidOperationException ( "Null or invalid directory from media encoder." ) ;
}
2023-02-22 08:08:35 +00:00
2024-01-14 11:11:16 +00:00
var images = _fileSystem . GetFiles ( imgTempDir , _trickplayImgExtensions , false , false )
. Select ( i = > i . FullName )
. OrderBy ( i = > i )
. ToList ( ) ;
2023-02-22 08:08:35 +00:00
2024-01-14 11:11:16 +00:00
// Create tiles
2024-04-30 11:41:46 +00:00
var trickplayInfo = CreateTiles ( images , actualWidth , options , outputDir ) ;
2023-02-22 08:08:35 +00:00
2024-01-14 11:11:16 +00:00
// Save tiles info
try
2023-02-22 08:08:35 +00:00
{
2024-01-14 11:11:16 +00:00
if ( trickplayInfo is not null )
{
trickplayInfo . ItemId = video . Id ;
await SaveTrickplayInfo ( trickplayInfo ) . ConfigureAwait ( false ) ;
_logger . LogInformation ( "Finished creation of trickplay files for {0}" , mediaPath ) ;
}
else
{
throw new InvalidOperationException ( "Null trickplay tiles info from CreateTiles." ) ;
}
2023-02-22 08:08:35 +00:00
}
2024-01-14 11:11:16 +00:00
catch ( Exception ex )
2023-02-22 08:08:35 +00:00
{
2024-01-14 11:11:16 +00:00
_logger . LogError ( ex , "Error while saving trickplay tiles info." ) ;
// Make sure no files stay in metadata folders on failure
// if tiles info wasn't saved.
Directory . Delete ( outputDir , true ) ;
2023-02-22 08:08:35 +00:00
}
}
catch ( Exception ex )
{
2024-01-14 11:11:16 +00:00
_logger . LogError ( ex , "Error creating trickplay images." ) ;
2023-02-22 08:08:35 +00:00
}
2024-01-14 11:11:16 +00:00
finally
2023-02-22 08:08:35 +00:00
{
2024-01-14 11:11:16 +00:00
if ( ! string . IsNullOrEmpty ( imgTempDir ) )
{
Directory . Delete ( imgTempDir , true ) ;
}
2023-02-22 08:08:35 +00:00
}
2023-05-01 19:51:05 +00:00
}
}
2023-02-22 08:08:35 +00:00
2023-07-01 23:16:41 +00:00
/// <inheritdoc />
2024-09-07 17:23:48 +00:00
public TrickplayInfo CreateTiles ( IReadOnlyList < string > images , int width , TrickplayOptions options , string outputDir )
2023-05-01 19:51:05 +00:00
{
if ( images . Count = = 0 )
{
2023-05-30 21:23:02 +00:00
throw new ArgumentException ( "Can't create trickplay from 0 images." ) ;
2023-05-01 19:51:05 +00:00
}
2023-02-22 08:08:35 +00:00
2024-07-15 12:44:14 +00:00
var workDir = Path . Combine ( _appPaths . TempDirectory , "trickplay_" + Guid . NewGuid ( ) . ToString ( "N" ) ) ;
2023-05-01 19:51:05 +00:00
Directory . CreateDirectory ( workDir ) ;
2023-02-22 08:08:35 +00:00
2023-06-27 00:40:10 +00:00
var trickplayInfo = new TrickplayInfo
2023-05-01 19:51:05 +00:00
{
Width = width ,
Interval = options . Interval ,
TileWidth = options . TileWidth ,
TileHeight = options . TileHeight ,
2023-06-27 00:40:10 +00:00
ThumbnailCount = images . Count ,
2023-05-30 21:23:02 +00:00
// Set during image generation
Height = 0 ,
2023-05-01 19:51:05 +00:00
Bandwidth = 0
} ;
2023-05-30 21:23:02 +00:00
/ *
2023-06-27 00:40:10 +00:00
* Generate trickplay tiles from sets of thumbnails
2023-05-30 21:23:02 +00:00
* /
var imageOptions = new ImageCollageOptions
2023-05-01 19:51:05 +00:00
{
2023-06-27 00:40:10 +00:00
Width = trickplayInfo . TileWidth ,
Height = trickplayInfo . TileHeight
2023-05-30 21:23:02 +00:00
} ;
2023-02-22 08:08:35 +00:00
2023-06-27 00:40:10 +00:00
var thumbnailsPerTile = trickplayInfo . TileWidth * trickplayInfo . TileHeight ;
var requiredTiles = ( int ) Math . Ceiling ( ( double ) images . Count / thumbnailsPerTile ) ;
2023-05-01 19:51:05 +00:00
2023-06-27 00:40:10 +00:00
for ( int i = 0 ; i < requiredTiles ; i + + )
2023-05-01 19:51:05 +00:00
{
2023-05-30 21:23:02 +00:00
// Set output/input paths
2023-06-27 00:40:10 +00:00
var tilePath = Path . Combine ( workDir , $"{i}.jpg" ) ;
2023-02-22 08:08:35 +00:00
2023-06-27 00:40:10 +00:00
imageOptions . OutputPath = tilePath ;
2024-09-07 17:23:48 +00:00
imageOptions . InputPaths = images . Skip ( i * thumbnailsPerTile ) . Take ( Math . Min ( thumbnailsPerTile , images . Count - ( i * thumbnailsPerTile ) ) ) . ToList ( ) ;
2023-02-22 08:08:35 +00:00
2023-05-30 21:23:02 +00:00
// Generate image and use returned height for tiles info
2023-06-27 00:40:10 +00:00
var height = _imageEncoder . CreateTrickplayTile ( imageOptions , options . JpegQuality , trickplayInfo . Width , trickplayInfo . Height ! = 0 ? trickplayInfo . Height : null ) ;
if ( trickplayInfo . Height = = 0 )
2023-02-22 08:08:35 +00:00
{
2023-06-27 00:40:10 +00:00
trickplayInfo . Height = height ;
2023-02-22 08:08:35 +00:00
}
2023-05-30 21:23:02 +00:00
// Update bitrate
2024-05-25 15:46:18 +00:00
var bitrate = ( int ) Math . Ceiling ( new FileInfo ( tilePath ) . Length * 8 m / trickplayInfo . TileWidth / trickplayInfo . TileHeight / ( trickplayInfo . Interval / 1000 m ) ) ;
2023-06-27 00:40:10 +00:00
trickplayInfo . Bandwidth = Math . Max ( trickplayInfo . Bandwidth , bitrate ) ;
2023-02-22 08:08:35 +00:00
}
2023-05-01 19:51:05 +00:00
/ *
* Move trickplay tiles to output directory
* /
2023-06-23 21:30:55 +00:00
Directory . CreateDirectory ( Directory . GetParent ( outputDir ) ! . FullName ) ;
2023-02-22 08:08:35 +00:00
2023-06-27 00:40:10 +00:00
// Replace existing tiles if they already exist
2023-05-01 19:51:05 +00:00
if ( Directory . Exists ( outputDir ) )
{
Directory . Delete ( outputDir , true ) ;
}
2023-02-23 02:17:54 +00:00
2024-09-07 17:23:48 +00:00
_fileSystem . MoveDirectory ( workDir , outputDir ) ;
2023-02-23 02:17:54 +00:00
2023-06-27 00:40:10 +00:00
return trickplayInfo ;
2023-05-01 19:51:05 +00:00
}
2023-02-22 08:08:35 +00:00
2023-05-01 19:51:05 +00:00
private bool CanGenerateTrickplay ( Video video , int interval )
{
var videoType = video . VideoType ;
if ( videoType = = VideoType . Iso | | videoType = = VideoType . Dvd | | videoType = = VideoType . BluRay )
{
return false ;
2023-02-22 08:08:35 +00:00
}
2023-05-01 19:51:05 +00:00
if ( video . IsPlaceHolder )
2023-02-22 08:08:35 +00:00
{
2023-05-01 19:51:05 +00:00
return false ;
2023-02-22 08:08:35 +00:00
}
2023-05-01 19:51:05 +00:00
if ( video . IsShortcut )
2023-02-22 08:08:35 +00:00
{
2023-05-01 19:51:05 +00:00
return false ;
2023-02-22 08:08:35 +00:00
}
2023-05-01 19:51:05 +00:00
if ( ! video . IsCompleteMedia )
2023-02-22 08:08:35 +00:00
{
2023-05-01 19:51:05 +00:00
return false ;
2023-02-22 08:08:35 +00:00
}
2023-05-01 19:51:05 +00:00
if ( ! video . RunTimeTicks . HasValue | | video . RunTimeTicks . Value < TimeSpan . FromMilliseconds ( interval ) . Ticks )
2023-02-22 08:08:35 +00:00
{
2023-05-01 19:51:05 +00:00
return false ;
2023-02-22 08:08:35 +00:00
}
2023-05-01 19:51:05 +00:00
var libraryOptions = _libraryManager . GetLibraryOptions ( video ) ;
2023-05-18 22:32:15 +00:00
if ( libraryOptions is null | | ! libraryOptions . EnableTrickplayImageExtraction )
2023-05-01 19:51:05 +00:00
{
return false ;
2023-02-22 08:08:35 +00:00
}
2023-05-01 19:51:05 +00:00
// Can't extract images if there are no video streams
return video . GetMediaStreams ( ) . Count > 0 ;
}
/// <inheritdoc />
2023-06-27 00:40:10 +00:00
public async Task < Dictionary < int , TrickplayInfo > > GetTrickplayResolutions ( Guid itemId )
2023-05-01 19:51:05 +00:00
{
2023-06-27 00:40:10 +00:00
var trickplayResolutions = new Dictionary < int , TrickplayInfo > ( ) ;
var dbContext = await _dbProvider . CreateDbContextAsync ( ) . ConfigureAwait ( false ) ;
await using ( dbContext . ConfigureAwait ( false ) )
{
var trickplayInfos = await dbContext . TrickplayInfos
. AsNoTracking ( )
. Where ( i = > i . ItemId . Equals ( itemId ) )
. ToListAsync ( )
. ConfigureAwait ( false ) ;
foreach ( var info in trickplayInfos )
{
trickplayResolutions [ info . Width ] = info ;
}
}
return trickplayResolutions ;
2023-05-01 19:51:05 +00:00
}
2024-09-07 17:23:48 +00:00
/// <inheritdoc />
public async Task < IReadOnlyList < Guid > > GetTrickplayItemsAsync ( )
{
List < Guid > trickplayItems ;
var dbContext = await _dbProvider . CreateDbContextAsync ( ) . ConfigureAwait ( false ) ;
await using ( dbContext . ConfigureAwait ( false ) )
{
trickplayItems = await dbContext . TrickplayInfos
. AsNoTracking ( )
. Select ( i = > i . ItemId )
. ToListAsync ( )
. ConfigureAwait ( false ) ;
}
return trickplayItems ;
}
2023-05-01 19:51:05 +00:00
/// <inheritdoc />
2023-06-27 00:40:10 +00:00
public async Task SaveTrickplayInfo ( TrickplayInfo info )
2023-05-01 19:51:05 +00:00
{
2023-06-27 00:40:10 +00:00
var dbContext = await _dbProvider . CreateDbContextAsync ( ) . ConfigureAwait ( false ) ;
await using ( dbContext . ConfigureAwait ( false ) )
{
var oldInfo = await dbContext . TrickplayInfos . FindAsync ( info . ItemId , info . Width ) . ConfigureAwait ( false ) ;
if ( oldInfo is not null )
{
dbContext . TrickplayInfos . Remove ( oldInfo ) ;
}
dbContext . Add ( info ) ;
await dbContext . SaveChangesAsync ( ) . ConfigureAwait ( false ) ;
}
2023-05-01 19:51:05 +00:00
}
/// <inheritdoc />
2023-09-04 19:30:20 +00:00
public async Task < Dictionary < string , Dictionary < int , TrickplayInfo > > > GetTrickplayManifest ( BaseItem item )
2023-05-01 19:51:05 +00:00
{
2023-09-04 19:30:20 +00:00
var trickplayManifest = new Dictionary < string , Dictionary < int , TrickplayInfo > > ( ) ;
2023-06-27 00:40:10 +00:00
foreach ( var mediaSource in item . GetMediaSources ( false ) )
{
var mediaSourceId = Guid . Parse ( mediaSource . Id ) ;
var trickplayResolutions = await GetTrickplayResolutions ( mediaSourceId ) . ConfigureAwait ( false ) ;
if ( trickplayResolutions . Count > 0 )
{
2023-09-04 19:30:20 +00:00
trickplayManifest [ mediaSource . Id ] = trickplayResolutions ;
2023-06-27 00:40:10 +00:00
}
}
return trickplayManifest ;
2023-05-01 19:51:05 +00:00
}
/// <inheritdoc />
2024-09-07 17:23:48 +00:00
public async Task < string > GetTrickplayTilePathAsync ( BaseItem item , int width , int index , bool saveWithMedia )
2023-05-01 19:51:05 +00:00
{
2024-09-07 17:23:48 +00:00
var trickplayResolutions = await GetTrickplayResolutions ( item . Id ) . ConfigureAwait ( false ) ;
if ( trickplayResolutions is not null & & trickplayResolutions . TryGetValue ( width , out var trickplayInfo ) )
{
return Path . Combine ( GetTrickplayDirectory ( item , trickplayInfo . TileWidth , trickplayInfo . TileHeight , width , saveWithMedia ) , index + ".jpg" ) ;
}
return string . Empty ;
2023-05-01 19:51:05 +00:00
}
2023-06-23 21:22:00 +00:00
/// <inheritdoc />
2023-06-27 00:40:10 +00:00
public async Task < string? > GetHlsPlaylist ( Guid itemId , int width , string? apiKey )
2023-06-23 21:22:00 +00:00
{
2023-06-27 00:40:10 +00:00
var trickplayResolutions = await GetTrickplayResolutions ( itemId ) . ConfigureAwait ( false ) ;
if ( trickplayResolutions is not null & & trickplayResolutions . TryGetValue ( width , out var trickplayInfo ) )
2023-06-23 21:22:00 +00:00
{
var builder = new StringBuilder ( 128 ) ;
2023-06-27 00:40:10 +00:00
if ( trickplayInfo . ThumbnailCount > 0 )
2023-06-23 21:22:00 +00:00
{
2024-02-12 17:30:47 +00:00
const string urlFormat = "{0}.jpg?MediaSourceId={1}&api_key={2}" ;
2023-06-23 21:22:00 +00:00
const string decimalFormat = "{0:0.###}" ;
2023-06-27 00:40:10 +00:00
var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}" ;
var layout = $"{trickplayInfo.TileWidth}x{trickplayInfo.TileHeight}" ;
var thumbnailsPerTile = trickplayInfo . TileWidth * trickplayInfo . TileHeight ;
var thumbnailDuration = trickplayInfo . Interval / 1000d ;
var infDuration = thumbnailDuration * thumbnailsPerTile ;
var tileCount = ( int ) Math . Ceiling ( ( decimal ) trickplayInfo . ThumbnailCount / thumbnailsPerTile ) ;
2023-06-23 21:22:00 +00:00
builder
. AppendLine ( "#EXTM3U" )
. Append ( "#EXT-X-TARGETDURATION:" )
2023-06-27 00:40:10 +00:00
. AppendLine ( tileCount . ToString ( CultureInfo . InvariantCulture ) )
2023-06-23 21:22:00 +00:00
. AppendLine ( "#EXT-X-VERSION:7" )
. AppendLine ( "#EXT-X-MEDIA-SEQUENCE:1" )
. AppendLine ( "#EXT-X-PLAYLIST-TYPE:VOD" )
. AppendLine ( "#EXT-X-IMAGES-ONLY" ) ;
2023-06-27 00:40:10 +00:00
for ( int i = 0 ; i < tileCount ; i + + )
2023-06-23 21:22:00 +00:00
{
2023-06-27 00:40:10 +00:00
// All tiles prior to the last must contain full amount of thumbnails (no black).
if ( i = = tileCount - 1 )
2023-06-23 21:22:00 +00:00
{
2023-06-27 00:40:10 +00:00
thumbnailsPerTile = trickplayInfo . ThumbnailCount - ( i * thumbnailsPerTile ) ;
infDuration = thumbnailDuration * thumbnailsPerTile ;
2023-06-23 21:22:00 +00:00
}
// EXTINF
builder
. Append ( "#EXTINF:" )
. AppendFormat ( CultureInfo . InvariantCulture , decimalFormat , infDuration )
. AppendLine ( "," ) ;
// EXT-X-TILES
builder
. Append ( "#EXT-X-TILES:RESOLUTION=" )
. Append ( resolution )
. Append ( ",LAYOUT=" )
. Append ( layout )
. Append ( ",DURATION=" )
2023-06-27 00:40:10 +00:00
. AppendFormat ( CultureInfo . InvariantCulture , decimalFormat , thumbnailDuration )
2023-06-23 21:22:00 +00:00
. AppendLine ( ) ;
// URL
builder
. AppendFormat (
CultureInfo . InvariantCulture ,
urlFormat ,
i . ToString ( CultureInfo . InvariantCulture ) ,
itemId . ToString ( "N" ) ,
apiKey )
. AppendLine ( ) ;
}
builder . AppendLine ( "#EXT-X-ENDLIST" ) ;
return builder . ToString ( ) ;
}
}
return null ;
}
2024-09-07 17:23:48 +00:00
/// <inheritdoc />
public string GetTrickplayDirectory ( BaseItem item , int tileWidth , int tileHeight , int width , bool saveWithMedia = false )
2023-05-01 19:51:05 +00:00
{
2024-09-07 17:23:48 +00:00
var path = saveWithMedia
? Path . Combine ( item . ContainingFolderPath , Path . ChangeExtension ( item . Path , ".trickplay" ) )
: Path . Combine ( item . GetInternalMetadataPath ( ) , "trickplay" ) ;
var subdirectory = string . Format (
CultureInfo . InvariantCulture ,
"{0} - {1}x{2}" ,
width . ToString ( CultureInfo . InvariantCulture ) ,
tileWidth . ToString ( CultureInfo . InvariantCulture ) ,
tileHeight . ToString ( CultureInfo . InvariantCulture ) ) ;
return Path . Combine ( path , subdirectory ) ;
2023-05-01 19:51:05 +00:00
}
2024-09-07 17:23:48 +00:00
private async Task < bool > HasTrickplayResolutionAsync ( Guid itemId , int width )
2023-05-01 19:51:05 +00:00
{
2024-09-07 17:23:48 +00:00
var dbContext = await _dbProvider . CreateDbContextAsync ( ) . ConfigureAwait ( false ) ;
await using ( dbContext . ConfigureAwait ( false ) )
2023-05-01 19:51:05 +00:00
{
2024-09-07 17:23:48 +00:00
return await dbContext . TrickplayInfos
. AsNoTracking ( )
. Where ( i = > i . ItemId . Equals ( itemId ) )
. AnyAsync ( i = > i . Width = = width )
. ConfigureAwait ( false ) ;
2023-02-22 08:08:35 +00:00
}
}
}