Compare commits

...

8 Commits

Author SHA1 Message Date
renovate[bot]
359b8a9751 Update github/codeql-action action to v4.36.3 2026-07-02 13:20:32 +00:00
m4st3r-0day
9cc25d133d Translated using Weblate (Albanian)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sq/
2026-07-01 20:34:37 +00:00
Bond-009
a43fd188dc Merge pull request #17175 from obrenoalvim/fix/use-leftjoin-ef10
Some checks failed
Format / format-check (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Use Enumerable.LeftJoin for activity log user query
2026-06-30 17:49:15 +02:00
Breno Alvim
f011529388 Use Enumerable.LeftJoin for activity log user query 2026-06-30 17:37:02 +02:00
Bond-009
d3ee1e84b1 Merge pull request #17170 from Shadowghost/better-bitrates
Some checks failed
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Rework bitrate reporting
2026-06-29 18:06:14 +02:00
Bond-009
1035f6a101 Merge pull request #15954 from IDisposable/fix/books
Fix Book collections speed issues
2026-06-29 18:05:55 +02:00
Marc Brooks
70b4589382 Fix Book collections scanning all items
Added static method GetBaseItemKindsForCollectionType in ItemsController (moved from ContentFolderImageProvider to be shared)

Added AudioBook to GetRepresentativeItemTypes for CollectionType.books for consistency

Added GetBooks to GetUserItems for CollectionType.books which gets BaseItemKind.Book and BaseItemKind.AudioBook

Move GetBaseItemKindsForCollectionType to DtoExtensions

Cleaned up the missing null checks and used new collection expressions.
Associate Person to Book and AudioBook for related items.
2026-06-26 11:25:58 -05:00
Shadowghost
d090c59939 Rework bitrate reporting 2026-06-23 17:47:17 +02:00
13 changed files with 507 additions and 96 deletions

View File

@@ -32,13 +32,13 @@ jobs:
dotnet-version: '10.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3

View File

@@ -71,6 +71,8 @@ namespace Emby.Server.Implementations.Dto
{
BaseItemKind.Person, [
BaseItemKind.Audio,
BaseItemKind.AudioBook,
BaseItemKind.Book,
BaseItemKind.Episode,
BaseItemKind.Movie,
BaseItemKind.LiveTvProgram,

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using Jellyfin.Api.Extensions;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration;
@@ -14,7 +15,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
@@ -28,38 +28,7 @@ namespace Emby.Server.Implementations.Images
{
var view = (CollectionFolder)item;
var viewType = view.CollectionType;
BaseItemKind[] includeItemTypes;
switch (viewType)
{
case CollectionType.movies:
includeItemTypes = new[] { BaseItemKind.Movie };
break;
case CollectionType.tvshows:
includeItemTypes = new[] { BaseItemKind.Series };
break;
case CollectionType.music:
includeItemTypes = new[] { BaseItemKind.MusicArtist }; // Music albums usually don't have dedicated backdrops, so use artist instead
break;
case CollectionType.musicvideos:
includeItemTypes = new[] { BaseItemKind.MusicVideo };
break;
case CollectionType.books:
includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
break;
case CollectionType.boxsets:
includeItemTypes = new[] { BaseItemKind.BoxSet };
break;
case CollectionType.homevideos:
case CollectionType.photos:
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
break;
default:
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
break;
}
var includeItemTypes = DtoExtensions.GetBaseItemKindsForCollectionType(viewType);
var recursive = viewType != CollectionType.playlists;
return view.GetItemList(new InternalItemsQuery
@@ -67,12 +36,9 @@ namespace Emby.Server.Implementations.Images
CollapseBoxSetItems = false,
Recursive = recursive,
DtoOptions = new DtoOptions(false),
ImageTypes = new[] { ImageType.Primary },
ImageTypes = [ImageType.Primary],
Limit = 8,
OrderBy = new[]
{
(ItemSortBy.Random, SortOrder.Ascending)
},
OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)],
IncludeItemTypes = includeItemTypes
});
}

View File

@@ -106,5 +106,7 @@
"TaskAudioNormalization": "Normalizimi i audios",
"TaskAudioNormalizationDescription": "Skannon skedarët për të dhëna të normalizimit të audios.",
"CleanupUserDataTaskDescription": "Pastron të gjitha të dhënat e përdorueseve (gjendja e shikimit, statusi i të preferuarave etj.) nga mediat që nuk janë më të pranishme për të paktën 90 ditë.",
"CleanupUserDataTask": "Veprim për pastrimin të dhënave të përdorueseve"
"CleanupUserDataTask": "Veprim për pastrimin të dhënave të përdorueseve",
"LyricDownloadFailureFromForItem": "Teksti i këngës nuk arriti të shkarkohej nga {0} për {1}",
"Original": "Origjinal"
}

View File

@@ -287,6 +287,8 @@ public class ItemsController : BaseJellyfinApiController
QueryResult<BaseItem> result;
Guid[] linkedChildAncestorIds = [];
includeItemTypes ??= [];
if (includeItemTypes.Length == 1
&& (includeItemTypes[0] == BaseItemKind.BoxSet || includeItemTypes[0] == BaseItemKind.Playlist)
&& item is not BoxSet
@@ -314,6 +316,7 @@ public class ItemsController : BaseJellyfinApiController
if (folder is IHasCollectionType hasCollectionType)
{
collectionType = hasCollectionType.CollectionType;
includeItemTypes = [.. includeItemTypes.Union(DtoExtensions.GetBaseItemKindsForCollectionType(collectionType))];
}
if (collectionType == CollectionType.playlists)

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Model.Entities;
@@ -9,6 +10,35 @@ namespace Jellyfin.Api.Extensions;
/// </summary>
public static class DtoExtensions
{
/// <summary>
/// Gets the BaseItemKind values associated with the specified CollectionType.
/// </summary>
/// <param name="collectionType">The collection type to map to BaseItemKind values.</param>
/// <returns>An array of BaseItemKind values that correspond to the collection type.</returns>
public static BaseItemKind[] GetBaseItemKindsForCollectionType(CollectionType? collectionType)
{
switch (collectionType)
{
case CollectionType.movies:
return [BaseItemKind.Movie];
case CollectionType.tvshows:
return [BaseItemKind.Series];
case CollectionType.music:
return [BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist];
case CollectionType.musicvideos:
return [BaseItemKind.MusicVideo];
case CollectionType.books:
return [BaseItemKind.Book, BaseItemKind.AudioBook];
case CollectionType.boxsets:
return [BaseItemKind.BoxSet];
case CollectionType.homevideos:
case CollectionType.photos:
return [BaseItemKind.Video, BaseItemKind.Photo];
default:
return [BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series];
}
}
/// <summary>
/// Add additional DtoOptions.
/// </summary>

View File

@@ -56,11 +56,11 @@ public class ActivityManager : IActivityManager
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
// TODO switch to LeftJoin in .NET 10.
var entries = from a in dbContext.ActivityLogs
join u in dbContext.Users on a.UserId equals u.Id into ugj
from u in ugj.DefaultIfEmpty()
select new ExpandedActivityLog { ActivityLog = a, Username = u.Username };
var entries = dbContext.ActivityLogs.LeftJoin(
dbContext.Users,
a => a.UserId,
u => u.Id,
(a, u) => new ExpandedActivityLog { ActivityLog = a, Username = u == null ? null : u.Username });
if (query.HasUserId is not null)
{

View File

@@ -61,6 +61,9 @@ namespace MediaBrowser.Controller.Entities
case CollectionType.folders:
return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), query);
case CollectionType.books:
return GetBooks(queryParent, user, query);
case CollectionType.tvshows:
return GetTvView(queryParent, user, query);
@@ -190,6 +193,17 @@ namespace MediaBrowser.Controller.Entities
return _libraryManager.GetItemsResult(query);
}
private QueryResult<BaseItem> GetBooks(Folder parent, User user, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.IncludeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
return _libraryManager.GetItemsResult(query);
}
private QueryResult<BaseItem> GetMovieMovies(Folder parent, User user, InternalItemsQuery query)
{
query.Recursive = true;

View File

@@ -254,16 +254,38 @@ namespace MediaBrowser.MediaEncoding.Probing
{
if (mediaStream.Type == MediaStreamType.Audio && !mediaStream.BitRate.HasValue)
{
mediaStream.BitRate = GetEstimatedAudioBitrate(mediaStream.Codec, mediaStream.Channels);
mediaStream.BitRate = GetEstimatedAudioBitrate(mediaStream.Codec, mediaStream.Profile, mediaStream.Channels);
}
}
var videoStreamsBitrate = info.MediaStreams.Where(i => i.Type == MediaStreamType.Video).Select(i => i.BitRate ?? 0).Sum();
// If ffprobe reported the container bitrate as being the same as the video stream bitrate, then it's wrong
if (videoStreamsBitrate == (info.Bitrate ?? 0))
// ffprobe frequently omits the per-stream video bitrate (common in MP4/MKV containers).
// Estimate the missing video bitrate as the container bitrate minus the combined stream bitrates.
var videoStreams = info.MediaStreams.Where(i => i.Type == MediaStreamType.Video).ToList();
if (info.Bitrate.HasValue
&& videoStreams.Count == 1
&& !videoStreams[0].BitRate.HasValue)
{
info.InferTotalBitrate(true);
var otherStreams = info.MediaStreams
.Where(i => i.Type != MediaStreamType.Video && !i.IsExternal)
.ToList();
// Only attribute the leftover bitrate to the video stream if every audio stream's bitrate is known.
var audioBitratesKnown = otherStreams
.Where(i => i.Type == MediaStreamType.Audio)
.All(i => i.BitRate.HasValue);
if (audioBitratesKnown)
{
var estimatedVideoBitrate = info.Bitrate.Value - otherStreams.Sum(i => i.BitRate ?? 0);
if (estimatedVideoBitrate > 0)
{
videoStreams[0].BitRate = estimatedVideoBitrate;
}
}
}
// If the container bitrate is still unknown, infer it from the sum of the streams.
info.InferTotalBitrate();
}
return info;
@@ -316,54 +338,34 @@ namespace MediaBrowser.MediaEncoding.Probing
return string.Join(',', splitFormat.Where(s => !string.IsNullOrEmpty(s)));
}
private static int? GetEstimatedAudioBitrate(string codec, int? channels)
internal static int? GetEstimatedAudioBitrate(string codec, string profile, int? channels)
{
if (!channels.HasValue)
if (!channels.HasValue || channels.Value < 1 || string.IsNullOrEmpty(codec))
{
return null;
}
var channelsValue = channels.Value;
// Rough typical bitrates used only as a fallback when ffprobe doesn't report a stream bitrate.
var channelCount = channels.Value;
var isMultichannel = channelCount > 2;
if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
return codec.ToLowerInvariant() switch
{
switch (channelsValue)
{
case <= 2:
return 192000;
case >= 5:
return 320000;
}
}
if (string.Equals(codec, "ac3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase))
{
switch (channelsValue)
{
case <= 2:
return 192000;
case >= 5:
return 640000;
}
}
if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "alac", StringComparison.OrdinalIgnoreCase))
{
switch (channelsValue)
{
case <= 2:
return 960000;
case >= 5:
return 2880000;
}
}
return null;
"aac" or "mp3" or "mp2" => isMultichannel ? 320000 : 192000,
"ac3" or "eac3" => isMultichannel ? 640000 : 192000,
"dts" or "dca" => IsDtsLossless(profile) ? channelCount * 700000 : (isMultichannel ? 1509000 : 768000),
"opus" => isMultichannel ? 256000 : 128000,
"vorbis" => isMultichannel ? 320000 : 160000,
"wmav1" or "wmav2" or "wmapro" => isMultichannel ? 384000 : 192000,
"flac" or "alac" => channelCount * 480000,
"truehd" or "mlp" => channelCount * 700000,
_ => null
};
}
private static bool IsDtsLossless(string profile)
=> profile is not null && profile.Contains("HD MA", StringComparison.OrdinalIgnoreCase);
private void FetchFromItunesInfo(string xml, MediaInfo info)
{
// Make things simpler and strip out the dtd
@@ -972,10 +974,12 @@ namespace MediaBrowser.MediaEncoding.Probing
bitrate = value;
}
// The bitrate info of FLAC musics and some videos is included in formatInfo.
// The bitrate info of FLAC audio is included in formatInfo.
// Don't do this for video streams: formatInfo.BitRate is the overall container
// bitrate (video + audio + subtitles + overhead), not the video bitrate.
if (bitrate == 0
&& formatInfo is not null
&& (stream.Type == MediaStreamType.Video || (isAudio && stream.Type == MediaStreamType.Audio)))
&& isAudio && stream.Type == MediaStreamType.Audio)
{
// If the stream info doesn't have a bitrate get the value from the media format info
if (int.TryParse(formatInfo.BitRate, CultureInfo.InvariantCulture, out value))
@@ -1260,9 +1264,16 @@ namespace MediaBrowser.MediaEncoding.Probing
}
var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION");
if (TimeSpan.TryParse(duration, out var parsedDuration))
if (!string.IsNullOrEmpty(duration))
{
return parsedDuration.TotalSeconds;
// Matroska DURATION tags use nanosecond precision (e.g. "00:00:05.023000000"), but
// TimeSpan only supports up to 7 fractional digits (ticks). Trim the surplus digits so
// these durations parse instead of being silently dropped.
duration = DurationOverPrecisionRegex().Replace(duration, "$1");
if (TimeSpan.TryParse(duration, CultureInfo.InvariantCulture, out var parsedDuration))
{
return parsedDuration.TotalSeconds;
}
}
return null;
@@ -1764,5 +1775,8 @@ namespace MediaBrowser.MediaEncoding.Probing
[GeneratedRegex("(?<name>.*) \\((?<instrument>.*)\\)")]
private static partial Regex PerformerRegex();
[GeneratedRegex(@"(\.\d{7})\d+")]
private static partial Regex DurationOverPrecisionRegex();
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
@@ -56,6 +57,43 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
public void IsNearSquarePixelSar_DetectsCorrectly(string? sar, bool expected)
=> Assert.Equal(expected, ProbeResultNormalizer.IsNearSquarePixelSar(sar));
[Theory]
// Lossy codecs, mono/stereo and multichannel.
[InlineData("aac", null, 2, 192000)]
[InlineData("mp3", null, 2, 192000)]
[InlineData("mp2", null, 2, 192000)]
[InlineData("aac", null, 6, 320000)]
[InlineData("ac3", null, 2, 192000)]
[InlineData("eac3", null, 6, 640000)]
[InlineData("opus", null, 2, 128000)]
[InlineData("vorbis", null, 6, 320000)]
[InlineData("wmav2", null, 2, 192000)]
// DTS: the lossy core (any non-MA profile, or none) is flat and caps at 5.1...
[InlineData("dts", null, 2, 768000)]
[InlineData("dts", "DTS", 6, 1509000)]
[InlineData("dts", "DTS-HD HRA", 8, 1509000)]
// ...while lossless DTS-HD MA scales per channel like other lossless codecs.
[InlineData("dts", "DTS-HD MA", 6, 4200000)]
[InlineData("dts", "DTS-HD MA + DTS:X", 8, 5600000)]
// Lossless codecs scale per channel.
[InlineData("flac", null, 2, 960000)]
[InlineData("flac", null, 6, 2880000)]
[InlineData("flac", null, 8, 3840000)]
[InlineData("alac", null, 6, 2880000)]
[InlineData("truehd", null, 2, 1400000)]
[InlineData("truehd", null, 6, 4200000)]
[InlineData("truehd", "Dolby TrueHD + Dolby Atmos", 8, 5600000)]
// 3-4 channel audio must use the multichannel estimate, not return null.
[InlineData("aac", null, 3, 320000)]
[InlineData("ac3", null, 4, 640000)]
// Codec matching is case-insensitive.
[InlineData("AAC", null, 2, 192000)]
// Unknown codec or unknown channel count cannot be estimated.
[InlineData("pcm_s16le", null, 2, null)]
[InlineData("aac", null, null, null)]
public void GetEstimatedAudioBitrate_ReturnsExpected(string codec, string? profile, int? channels, int? expected)
=> Assert.Equal(expected, ProbeResultNormalizer.GetEstimatedAudioBitrate(codec, profile, channels));
[Fact]
public void GetMediaInfo_MetaData_Success()
{
@@ -71,7 +109,10 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("4:3", res.VideoStream.AspectRatio);
Assert.Equal(25f, res.VideoStream.AverageFrameRate);
Assert.Equal(8, res.VideoStream.BitDepth);
Assert.Equal(69432, res.VideoStream.BitRate);
// ffprobe reports no per-stream video bitrate here. The container bitrate must not be
// misreported as the video bitrate, and the other streams' bitrates exceed the container
// bitrate in this sample, so no sensible video bitrate can be inferred (see #16248).
Assert.Null(res.VideoStream.BitRate);
Assert.Equal("h264", res.VideoStream.Codec);
Assert.Equal("1/50", res.VideoStream.CodecTimeBase);
Assert.Equal(240, res.VideoStream.Height);
@@ -321,6 +362,73 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.True(res.VideoStream.IsDefault);
}
[Fact]
public void GetMediaInfo_MissingVideoBitrate_EstimatedFromContainer()
{
// ffprobe did not report a per-stream video bitrate. The video bitrate must be estimated
// as the container bitrate minus the other (audio) stream bitrates, not reported as the
// whole container bitrate (see #16248).
var bytes = File.ReadAllBytes("Test Data/Probing/video_missing_video_bitrate.json");
var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_missing_video_bitrate.mp4", MediaProtocol.File);
Assert.Equal(2, res.MediaStreams.Count);
Assert.NotNull(res.VideoStream);
Assert.Equal(MediaStreamType.Video, res.VideoStream.Type);
var audioStream = res.MediaStreams.First(i => i.Type == MediaStreamType.Audio);
Assert.Equal(128000, audioStream.BitRate);
// Container bitrate (5128000) minus the audio bitrate (128000).
Assert.Equal(5000000, res.VideoStream.BitRate);
// The container bitrate itself must remain the overall container bitrate.
Assert.Equal(5128000, res.Bitrate);
}
[Fact]
public void GetMediaInfo_NanosecondDurationTag_BitrateComputedFromBytes()
{
// The stream carries NUMBER_OF_BYTES and a nanosecond-precision DURATION tag but no
// bitrate. TimeSpan only supports 7 fractional digits, so the 9-digit DURATION must be
// trimmed for the duration to parse and the bitrate to be computed (bytes * 8 / seconds).
var bytes = File.ReadAllBytes("Test Data/Probing/video_nanosecond_duration_bitrate.json");
var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_nanosecond_duration_bitrate.mkv", MediaProtocol.File);
Assert.NotNull(res.VideoStream);
// 10000000 bytes * 8 / 100 seconds.
Assert.Equal(800000, res.VideoStream.BitRate);
}
[Fact]
public void GetMediaInfo_MissingVideoBitrate_UnknownAudioBitrate_NotEstimated()
{
// ffprobe reported no per-stream video bitrate and the audio bitrate cannot be estimated
// (the audio stream has no channel count, so GetEstimatedAudioBitrate returns null). The
// video bitrate must be left unset rather than wrongly absorbing the unaccounted audio
// bitrate (see #16248).
var bytes = File.ReadAllBytes("Test Data/Probing/video_missing_video_bitrate_unknown_audio.json");
var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_missing_video_bitrate_unknown_audio.mp4", MediaProtocol.File);
Assert.Equal(2, res.MediaStreams.Count);
Assert.NotNull(res.VideoStream);
Assert.Null(res.VideoStream.BitRate);
var audioStream = res.MediaStreams.First(i => i.Type == MediaStreamType.Audio);
Assert.Null(audioStream.BitRate);
// The overall container bitrate is still reported.
Assert.Equal(5128000, res.Bitrate);
}
[Fact]
public void GetMediaInfo_VideoWithSingleFrameMjpeg_Success()
{

View File

@@ -0,0 +1,113 @@
{
"streams": [
{
"index": 0,
"codec_name": "h264",
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"profile": "High",
"codec_type": "video",
"codec_time_base": "1/48",
"codec_tag_string": "avc1",
"codec_tag": "0x31637661",
"width": 1920,
"height": 1080,
"coded_width": 1920,
"coded_height": 1080,
"closed_captions": 0,
"has_b_frames": 2,
"pix_fmt": "yuv420p",
"level": 40,
"chroma_location": "left",
"refs": 1,
"is_avc": "true",
"nal_length_size": "4",
"r_frame_rate": "24/1",
"avg_frame_rate": "24/1",
"time_base": "1/12288",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 3686400,
"duration": "300.000000",
"bits_per_raw_sample": "8",
"nb_frames": "7200",
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
},
"tags": {
"language": "und",
"handler_name": "VideoHandler"
}
},
{
"index": 1,
"codec_name": "aac",
"codec_long_name": "AAC (Advanced Audio Coding)",
"profile": "LC",
"codec_type": "audio",
"codec_time_base": "1/48000",
"codec_tag_string": "mp4a",
"codec_tag": "0x6134706d",
"sample_fmt": "fltp",
"sample_rate": "48000",
"channels": 2,
"channel_layout": "stereo",
"bits_per_sample": 0,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 14400000,
"duration": "300.000000",
"bit_rate": "128000",
"nb_frames": "14063",
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
},
"tags": {
"language": "eng",
"handler_name": "SoundHandler"
}
}
],
"format": {
"filename": "test.1080p.mp4",
"nb_streams": 2,
"nb_programs": 0,
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"format_long_name": "QuickTime / MOV",
"start_time": "0.000000",
"duration": "300.000000",
"size": "192000000",
"bit_rate": "5128000",
"probe_score": 100,
"tags": {
"major_brand": "isom",
"minor_version": "512",
"compatible_brands": "isomiso2avc1mp41",
"encoder": "Lavf58.20.100"
}
}
}

View File

@@ -0,0 +1,110 @@
{
"streams": [
{
"index": 0,
"codec_name": "h264",
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"profile": "High",
"codec_type": "video",
"codec_time_base": "1/48",
"codec_tag_string": "avc1",
"codec_tag": "0x31637661",
"width": 1920,
"height": 1080,
"coded_width": 1920,
"coded_height": 1080,
"closed_captions": 0,
"has_b_frames": 2,
"pix_fmt": "yuv420p",
"level": 40,
"chroma_location": "left",
"refs": 1,
"is_avc": "true",
"nal_length_size": "4",
"r_frame_rate": "24/1",
"avg_frame_rate": "24/1",
"time_base": "1/12288",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 3686400,
"duration": "300.000000",
"bits_per_raw_sample": "8",
"nb_frames": "7200",
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
},
"tags": {
"language": "und",
"handler_name": "VideoHandler"
}
},
{
"index": 1,
"codec_name": "dts",
"codec_long_name": "DCA (DTS Coherent Acoustics)",
"profile": "DTS-HD MA",
"codec_type": "audio",
"codec_time_base": "1/48000",
"codec_tag_string": "[0][0][0][0]",
"codec_tag": "0x0000",
"sample_fmt": "s32p",
"sample_rate": "48000",
"bits_per_sample": 0,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 14400000,
"duration": "300.000000",
"nb_frames": "14063",
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
},
"tags": {
"language": "eng",
"handler_name": "SoundHandler"
}
}
],
"format": {
"filename": "test.1080p.mp4",
"nb_streams": 2,
"nb_programs": 0,
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"format_long_name": "QuickTime / MOV",
"start_time": "0.000000",
"duration": "300.000000",
"size": "192000000",
"bit_rate": "5128000",
"probe_score": 100,
"tags": {
"major_brand": "isom",
"minor_version": "512",
"compatible_brands": "isomiso2avc1mp41",
"encoder": "Lavf58.20.100"
}
}
}

View File

@@ -0,0 +1,49 @@
{
"streams": [
{
"index": 0,
"codec_name": "h264",
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"profile": "High",
"codec_type": "video",
"codec_tag_string": "[0][0][0][0]",
"codec_tag": "0x0000",
"width": 1920,
"height": 1080,
"coded_width": 1920,
"coded_height": 1080,
"has_b_frames": 2,
"pix_fmt": "yuv420p",
"level": 40,
"r_frame_rate": "24/1",
"avg_frame_rate": "24/1",
"time_base": "1/1000",
"start_pts": 0,
"start_time": "0.000000",
"disposition": {
"default": 1
},
"tags": {
"language": "eng",
"BPS-eng": "",
"DURATION-eng": "00:01:40.000000000",
"NUMBER_OF_FRAMES-eng": "2400",
"NUMBER_OF_BYTES-eng": "10000000"
}
}
],
"format": {
"filename": "video_nanosecond_duration_bitrate.mkv",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "matroska,webm",
"format_long_name": "Matroska / WebM",
"start_time": "0.000000",
"duration": "100.000000",
"size": "10001000",
"probe_score": 100,
"tags": {
"encoder": "libebml v1.4.2 + libmatroska v1.6.4"
}
}
}