Compare commits

..

36 Commits

Author SHA1 Message Date
Jellyfin Release Bot
730b01fb14 Bump version to 10.9.3 2024-05-26 20:00:29 -04:00
gnattu
45e8872cc0 Extract media attachment one by one if the filename appears to be a path (#11812) 2024-05-26 11:40:28 -06:00
Joshua M. Boniface
bcf884ccfa Update GitHub workflows from Master
Backport fixes from:
 - #11780
 - #11779
 - #11769
 - #11733
 - #11515
2024-05-25 11:55:48 -04:00
gnattu
2eece01acc Filter invalid IPs on external interface matching (#11766) 2024-05-25 09:44:03 -06:00
gnattu
ef985896e2 Use SharedStream for LiveTV more restrictively (#11805) 2024-05-25 09:43:53 -06:00
Nyanmisaka
5e7514243c Fix the IOSurf error in QSV transcoding (#11830) 2024-05-25 09:00:30 -06:00
NotSaifA
4a344bebc0 Trickplay: kill ffmpeg when task is cancelled (#11790) 2024-05-24 07:50:55 -06:00
Tim Eisele
b2d54b82fa Improve reliability of HasChanged check (#11792) 2024-05-24 07:50:09 -06:00
gnattu
e7b1162cb3 Force more compatible transcoding profile for LiveTV (#11801) 2024-05-24 07:36:59 -06:00
Tim Eisele
d89e5a0074 Exclude virtual items from DateLastMediaAdded calculation (#11804) 2024-05-24 07:36:39 -06:00
Tim Eisele
4a54e5ddeb Add Canceled to ended state (#11808) 2024-05-24 07:36:19 -06:00
Bond-009
d9232e05f1 Merge pull request #11798 from gnattu/fix-trickplay-image-height
Recalculate trickplay image height for anamorphic videos
2024-05-24 14:19:37 +02:00
Bond-009
52be8be28f Merge pull request #11754 from Shadowghost/fix-bd-chapter-images
Fix BD/DVD folder chapter image extraction
2024-05-24 14:18:10 +02:00
Bond-009
ab6c2424db Merge pull request #11802 from crobibero/nullable-match-term
Mark SearchHint.MatchedTerm as nullable
2024-05-24 14:17:59 +02:00
Bond-009
eb437e7163 Merge pull request #11799 from nyanmisaka/fix-va-vk-interop-chk
Disable VA-VK interop on not supported kernel versions
2024-05-24 14:17:51 +02:00
Bond-009
2ddf2a7866 Merge pull request #11781 from Bond-009/orderts
Retain order blu-ray segments
2024-05-24 14:16:45 +02:00
Bond-009
60232ce9be Merge pull request #11788 from gnattu/override-too-small-trickplay-image-interval
Override too small trickplay image interval
2024-05-24 14:16:33 +02:00
gnattu
952995f796 Ignore NullOrEmpty imageStream.AspectRatio
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-23 23:08:29 +08:00
gnattu
933a285bf5 Use single quote
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-23 23:03:35 +08:00
gnattu
f8da69f8e5 Allow decimal aspect ratio
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-23 23:02:57 +08:00
Cody Robibero
d5e29bfce3 here you go Niels 2024-05-23 07:16:56 -06:00
nyanmisaka
ab36c4c011 Disable VA-VK interop on not supported kernel versions
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2024-05-23 14:53:00 +08:00
gnattu
c6e29647fc Recalculate trickplay image height for anamorphic videos
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-23 12:36:43 +08:00
gnattu
fa2bff30f6 Explicitly use decimal
Co-authored-by: Cody Robibero <cody@robibe.ro>
2024-05-22 21:17:00 +08:00
gnattu
447f73caf4 Fix bitrate calculation accuracy
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-22 13:41:54 +08:00
gnattu
86129589ef Override too small trickplay image interval
Some users may set this value too low, causing the trickplay generation to fail. Reset this interval to the minimum valid value of 1 second when the user-configured value is too small to prevent generation failures.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-22 13:35:28 +08:00
Niels van Velzen
06a5ddda5e Merge pull request #11774 from Bond-009/downmix
Apply audio boost when downmixing regardless of downmixalgo
2024-05-21 21:40:22 +02:00
Bond_009
6777f47e0e Retain order blu-ray segments 2024-05-21 20:19:15 +02:00
Bond_009
cf04e1d8e5 Apply audio boost when downmixing regardless of downmixalgo
This is what I'd expect from the naming
Should fix #11771
2024-05-21 16:24:43 +02:00
Bond-009
d608f1e3cc Merge pull request #11713 from gnattu/fix-vt-h264-profile
Fix VideoToolbox H264 constrained profile option
2024-05-21 13:58:01 +02:00
Bond-009
86f5c93434 Merge pull request #11739 from Shadowghost/fix-trickplay
Do not run trickplay on scan if disabled
2024-05-21 13:57:41 +02:00
Bond-009
4fcbeef5e6 Merge pull request #11738 from crobibero/non-user-session
Don't require user when getting current session
2024-05-21 13:57:31 +02:00
Shadowghost
77abafca8e Fix BD/DVD folder chapter image extraction 2024-05-20 13:19:05 +02:00
Shadowghost
2cebd5e05f Do not run trickplay on scan if disabled 2024-05-19 16:52:59 +02:00
Cody Robibero
b19b346670 Don't require user when getting current session 2024-05-19 08:25:38 -06:00
gnattu
53de8c0805 Fix VideoToolbox H264 constrained profile option
Like a lot of other encoders they need an underscore between two words.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-18 19:53:23 +08:00
30 changed files with 316 additions and 145 deletions

View File

@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup .NET
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6

View File

@@ -3,6 +3,8 @@ on:
push:
branches:
- master
tags:
- 'v*'
pull_request_target:
permissions: {}
@@ -14,7 +16,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -39,7 +41,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -99,11 +101,24 @@ jobs:
- id: read-diff
name: Read openapi-diff output
run: |
# Read and fix markdown
body=$(cat openapi-changes.md)
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
# Write to workflow summary
echo "$body" >> $GITHUB_STEP_SUMMARY
# Set ApiChanged var
if [ "$body" != '' ]; then
echo "ApiChanged=1" >> "$GITHUB_OUTPUT"
else
echo "ApiChanged=0" >> "$GITHUB_OUTPUT"
fi
# Add header/footer for diff comment
echo '<!--openapi-diff-workflow-comment-->' > openapi-changes-reply.md
echo "<details>" >> openapi-changes-reply.md
echo "<summary>Changes in OpenAPI specification found. Expand to see details.</summary>" >> openapi-changes-reply.md
echo "" >> openapi-changes-reply.md
echo "$body" >> openapi-changes-reply.md
echo "" >> openapi-changes-reply.md
echo "</details>" >> openapi-changes-reply.md
- name: Find difference comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
id: find-comment
@@ -113,22 +128,15 @@ jobs:
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ steps.read-diff.outputs.body != '' }}
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body: |
<!--openapi-diff-workflow-comment-->
<details>
<summary>Changes in OpenAPI specification found. Expand to see details.</summary>
${{ steps.read-diff.outputs.body }}
</details>
body-path: openapi-changes-reply.md
- name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
@@ -138,11 +146,9 @@ jobs:
No changes to OpenAPI specification found. See history of this comment for previous changes.
publish:
publish-unstable:
name: OpenAPI - Publish Unstable Spec
if: |
github.event_name != 'pull_request_target' &&
contains(github.repository_owner, 'jellyfin')
if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- openapi-head
@@ -201,3 +207,65 @@ jobs:
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
fi
) 200>/run/workflows/openapi-unstable.lock
publish-stable:
name: OpenAPI - Publish Stable Spec
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- openapi-head
steps:
- name: Set version number
id: version
run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (stable) to repository server
uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
source: openapi-head/openapi.json
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
debug: false
script_stop: false
script: |
if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows
sudo chown ${{ secrets.REPO_USER }} /run/workflows
fi
(
flock -x -w 300 200 || exit 1
TGT_DIR="/srv/repository/main/openapi"
LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then
rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
exit 0
fi
# Move new spec into place
sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
# Delete previous jellyfin-openapi-stable_previous.json
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json
sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Create new jellyfin-openapi-stable.json symlink
sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json
# Check that the previous openapi stable spec link is correct
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json
fi
) 200>/run/workflows/openapi-stable.lock

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
@@ -34,7 +34,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@2a2d60ea1c7e811f54684179af6ac1ae8c1ce69a # 5.2.5
uses: danielpalme/ReportGenerator-GitHub-Action@6b06171d1a131e7fd85121120a1c00c1ed03e033 # 5.3.0
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"

View File

@@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -51,7 +51,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -128,7 +128,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: pull in script
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
repository: jellyfin/jellyfin-triage-script
- name: install python

View File

@@ -10,7 +10,7 @@ jobs:
issues: write
steps:
- name: pull in script
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
repository: jellyfin/jellyfin-triage-script
- name: install python

View File

@@ -15,7 +15,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
uses: eps1lon/actions-label-merge-conflict@e62d7a53ff8be8b97684bffb6cfbbf3fc1115e2e # v3.0.0
uses: eps1lon/actions-label-merge-conflict@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'

View File

@@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
ref: ${{ env.TAG_BRANCH }}

View File

@@ -183,6 +183,7 @@
- [btopherjohnson](https://github.com/btopherjohnson)
- [GeorgeH005](https://github.com/GeorgeH005)
- [Vedant](https://github.com/viktory36/)
- [NotSaifA](https://github.com/NotSaifA)
# Emby Contributors

View File

@@ -36,7 +36,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.9.2</VersionPrefix>
<VersionPrefix>10.9.3</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -10,7 +10,7 @@ namespace Emby.Naming.TV;
public static class TvParserHelpers
{
private static readonly string[] _continuingState = ["Pilot", "Returning Series", "Returning"];
private static readonly string[] _endedState = ["Cancelled"];
private static readonly string[] _endedState = ["Cancelled", "Canceled"];
/// <summary>
/// Tries to parse a string into <see cref="SeriesStatus"/>.

View File

@@ -385,6 +385,19 @@ public class MediaInfoHelper
/// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
{
// Enforce more restrictive transcoding profile for LiveTV due to compatability reasons
// Cap the MaxStreamingBitrate to 20Mbps, because we are unable to reliably probe source bitrate,
// which will cause the client to request extremely high bitrate that may fail the player/encoder
request.MaxStreamingBitrate = request.MaxStreamingBitrate > 20000000 ? 20000000 : request.MaxStreamingBitrate;
if (request.DeviceProfile is not null)
{
// Remove all fmp4 transcoding profiles, because it causes playback error and/or A/V sync issues
// Notably: Some channels won't play on FireFox and LG webOs
// Some channels from HDHomerun will experience A/V sync issues
request.DeviceProfile.TranscodingProfiles = request.DeviceProfile.TranscodingProfiles.Where(p => !string.Equals(p.Container, "mp4", StringComparison.OrdinalIgnoreCase)).ToArray();
}
var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
var profile = request.DeviceProfile;

View File

@@ -120,7 +120,12 @@ public static class RequestHelpers
internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
{
userId ??= httpContext.User.GetUserId();
var user = userManager.GetUserById(userId.Value);
User? user = null;
if (!userId.IsNullOrEmpty())
{
user = userManager.GetUserById(userId.Value);
}
var session = await sessionManager.LogSessionActivity(
httpContext.User.GetClient(),
httpContext.User.GetVersion(),

View File

@@ -18,7 +18,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.9.2</VersionPrefix>
<VersionPrefix>10.9.3</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -81,6 +81,12 @@ public class TrickplayManager : ITrickplayManager
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
var options = _config.Configuration.TrickplayOptions;
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;
}
foreach (var width in options.WidthResolutions)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -267,7 +273,7 @@ public class TrickplayManager : ITrickplayManager
}
// Update bitrate
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000));
var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
}

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId>
<VersionPrefix>10.9.2</VersionPrefix>
<VersionPrefix>10.9.3</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -58,6 +58,11 @@ public static class NetworkConstants
/// </summary>
public static readonly IPNetwork IPv4RFC1918PrivateClassC = new IPNetwork(IPAddress.Parse("192.168.0.0"), 16);
/// <summary>
/// IPv4 Link-Local as defined in RFC 3927.
/// </summary>
public static readonly IPNetwork IPv4RFC3927LinkLocal = new IPNetwork(IPAddress.Parse("169.254.0.0"), 16);
/// <summary>
/// IPv6 loopback as defined in RFC 4291.
/// </summary>

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.9.2</VersionPrefix>
<VersionPrefix>10.9.3</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -55,6 +55,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minKerneli915Hang = new Version(5, 18);
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15);
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
@@ -680,16 +681,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return -1;
}
public string GetInputPathArgument(EncodingJobInfo state)
{
return state.MediaSource.VideoType switch
{
VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource),
VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource),
_ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource)
};
}
/// <summary>
/// Gets the audio encoder.
/// </summary>
@@ -1005,7 +996,8 @@ namespace MediaBrowser.Controller.MediaEncoding
Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc");
if (IsVulkanFullSupported()
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
{
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
@@ -1203,7 +1195,7 @@ namespace MediaBrowser.Controller.MediaEncoding
else
{
arg.Append(" -i ")
.Append(GetInputPathArgument(state));
.Append(_mediaEncoder.GetInputPathArgument(state));
}
// sub2video for external graphical subtitles
@@ -2083,6 +2075,18 @@ namespace MediaBrowser.Controller.MediaEncoding
profile = "constrained_high";
}
if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase)
&& profile.Contains("constrainedbaseline", StringComparison.OrdinalIgnoreCase))
{
profile = "constrained_baseline";
}
if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase)
&& profile.Contains("constrainedhigh", StringComparison.OrdinalIgnoreCase))
{
profile = "constrained_high";
}
if (!string.IsNullOrEmpty(profile))
{
// Currently there's no profile option in av1_nvenc encoder
@@ -2629,10 +2633,14 @@ namespace MediaBrowser.Controller.MediaEncoding
&& state.AudioStream.Channels.HasValue
&& state.AudioStream.Channels.Value == 6)
{
if (!encodingOptions.DownMixAudioBoost.Equals(1))
{
filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
}
switch (encodingOptions.DownMixStereoAlgorithm)
{
case DownMixStereoAlgorithms.Dave750:
filters.Add("volume=4.25");
filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3");
break;
case DownMixStereoAlgorithms.NightmodeDialogue:
@@ -2640,11 +2648,6 @@ namespace MediaBrowser.Controller.MediaEncoding
break;
case DownMixStereoAlgorithms.None:
default:
if (!encodingOptions.DownMixAudioBoost.Equals(1))
{
filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
}
break;
}
}
@@ -3161,7 +3164,9 @@ namespace MediaBrowser.Controller.MediaEncoding
int? requestedMaxHeight)
{
var isV4l2 = string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase);
var isMjpeg = videoEncoder is not null && videoEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
var scaleVal = isV4l2 ? 64 : 2;
var targetAr = isMjpeg ? "(a*sar)" : "a"; // manually calculate AR when using mjpeg encoder
// If fixed dimensions were supplied
if (requestedWidth.HasValue && requestedHeight.HasValue)
@@ -3190,10 +3195,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
@"scale=trunc(min(max(iw\,ih*a)\,min({0}\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\,ih)\,min({0}/a\,{1}))/2)*2",
@"scale=trunc(min(max(iw\,ih*{3})\,min({0}\,{1}*{3}))/{2})*{2}:trunc(min(max(iw/{3}\,ih)\,min({0}/{3}\,{1}))/2)*2",
maxWidthParam,
maxHeightParam,
scaleVal);
scaleVal,
targetAr);
}
// If a fixed width was requested
@@ -3209,8 +3215,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
"scale={0}:trunc(ow/a/2)*2",
widthParam);
"scale={0}:trunc(ow/{1}/2)*2",
widthParam,
targetAr);
}
// If a fixed height was requested
@@ -3220,9 +3227,10 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
"scale=trunc(oh*a/{1})*{1}:{0}",
"scale=trunc(oh*{2}/{1})*{1}:{0}",
heightParam,
scaleVal);
scaleVal,
targetAr);
}
// If a max width was requested
@@ -3232,9 +3240,10 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
@"scale=trunc(min(max(iw\,ih*a)\,{0})/{1})*{1}:trunc(ow/a/2)*2",
@"scale=trunc(min(max(iw\,ih*{2})\,{0})/{1})*{1}:trunc(ow/{2}/2)*2",
maxWidthParam,
scaleVal);
scaleVal,
targetAr);
}
// If a max height was requested
@@ -3244,9 +3253,10 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(
CultureInfo.InvariantCulture,
@"scale=trunc(oh*a/{1})*{1}:min(max(iw/a\,ih)\,{0})",
@"scale=trunc(oh*{2}/{1})*{1}:min(max(iw/{2}\,ih)\,{0})",
maxHeightParam,
scaleVal);
scaleVal,
targetAr);
}
return string.Empty;
@@ -4291,6 +4301,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// map from qsv to vaapi.
mainFilters.Add("hwmap=derive_device=vaapi");
mainFilters.Add("format=vaapi");
}
var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12");
@@ -4300,6 +4311,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// map from vaapi to qsv.
mainFilters.Add("hwmap=derive_device=qsv");
mainFilters.Add("format=qsv");
}
}
@@ -4474,7 +4486,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// prefered vaapi + vulkan filters pipeline
if (_mediaEncoder.IsVaapiDeviceAmd
&& isVaapiVkSupported
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
{
// AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support.
return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);

View File

@@ -245,6 +245,21 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>A playlist.</returns>
IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path);
/// <summary>
/// Gets the input path argument from <see cref="EncodingJobInfo"/>.
/// </summary>
/// <param name="state">The <see cref="EncodingJobInfo"/>.</param>
/// <returns>The input path argument.</returns>
string GetInputPathArgument(EncodingJobInfo state);
/// <summary>
/// Gets the input path argument.
/// </summary>
/// <param name="path">The item path.</param>
/// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
/// <returns>The input path argument.</returns>
string GetInputPathArgument(string path, MediaSourceInfo mediaSource);
/// <summary>
/// Generates a FFmpeg concat config for the source.
/// </summary>

View File

@@ -89,15 +89,28 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath,
CancellationToken cancellationToken)
{
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase));
if (shouldExtractOneByOne)
{
if (!Directory.Exists(outputPath))
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
foreach (var i in attachmentIndexes)
{
await ExtractAllAttachmentsInternal(
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
outputPath,
false,
cancellationToken).ConfigureAwait(false);
var newName = Path.Join(outputPath, i.ToString(CultureInfo.InvariantCulture));
await ExtractAttachment(inputFile, mediaSource, i, newName, cancellationToken).ConfigureAwait(false);
}
}
else
{
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
{
if (!Directory.Exists(outputPath))
{
await ExtractAllAttachmentsInternal(
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
outputPath,
false,
cancellationToken).ConfigureAwait(false);
}
}
}
}

View File

@@ -30,10 +30,8 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using static Nikse.SubtitleEdit.Core.Common.IfoParser;
namespace MediaBrowser.MediaEncoding.Encoder
{
@@ -621,7 +619,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
ImageFormat? targetFormat,
CancellationToken cancellationToken)
{
var inputArgument = GetInputArgument(inputFile, mediaSource);
var inputArgument = GetInputPathArgument(inputFile, mediaSource);
if (!isAudio)
{
@@ -824,6 +822,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
options.EnableTonemapping = false;
}
if (imageStream.Width is not null && imageStream.Height is not null && !string.IsNullOrEmpty(imageStream.AspectRatio))
{
// For hardware trickplay encoders, we need to re-calculate the size because they used fixed scale dimensions
var darParts = imageStream.AspectRatio.Split(':');
var (wa, ha) = (double.Parse(darParts[0], CultureInfo.InvariantCulture), double.Parse(darParts[1], CultureInfo.InvariantCulture));
// When dimension / DAR does not equal to 1:1, then the frames are most likely stored stretched.
// Note: this might be incorrect for 3D videos as the SAR stored might be per eye instead of per video, but we really can do little about it.
var shouldResetHeight = Math.Abs((imageStream.Width.Value * ha) - (imageStream.Height.Value * wa)) > .05;
if (shouldResetHeight)
{
// SAR = DAR * Height / Width
// RealHeight = Height / SAR = Height / (DAR * Height / Width) = Width / DAR
imageStream.Height = Convert.ToInt32(imageStream.Width.Value * ha / wa);
}
}
var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) };
var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
{
@@ -945,7 +959,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
while (isResponsive)
while (isResponsive && !cancellationToken.IsCancellationRequested)
{
try
{
@@ -959,8 +973,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
// We don't actually expect the process to be finished in one timeout span, just that one image has been generated.
}
cancellationToken.ThrowIfCancellationRequested();
var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
isResponsive = jpegCount > lastCount;
@@ -969,7 +981,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (!ranToCompletion)
{
_logger.LogInformation("Stopping trickplay extraction due to process inactivity.");
if (!isResponsive)
{
_logger.LogInformation("Trickplay process unresponsive.");
}
_logger.LogInformation("Stopping trickplay extraction.");
StopProcess(processWrapper, 1000);
}
}
@@ -1137,16 +1154,29 @@ namespace MediaBrowser.MediaEncoding.Encoder
var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files;
// Get all files from the BDMV/STREAMING directory
var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM"));
// Only return playable local .m2ts files
return directoryFiles
.Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase))
return validPlaybackFiles
.Select(f => _fileSystem.GetFileInfo(Path.Join(path, "BDMV", "STREAM", f)))
.Where(f => f.Exists)
.Select(f => f.FullName)
.Order()
.ToList();
}
/// <inheritdoc />
public string GetInputPathArgument(EncodingJobInfo state)
=> GetInputPathArgument(state.MediaPath, state.MediaSource);
/// <inheritdoc />
public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
{
return mediaSource.VideoType switch
{
VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null).ToList(), mediaSource),
VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path).ToList(), mediaSource),
_ => GetInputArgument(path, mediaSource)
};
}
/// <inheritdoc />
public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
{

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Model</PackageId>
<VersionPrefix>10.9.2</VersionPrefix>
<VersionPrefix>10.9.3</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -43,7 +43,7 @@ namespace MediaBrowser.Model.Search
/// Gets or sets the matched term.
/// </summary>
/// <value>The matched term.</value>
public string MatchedTerm { get; set; }
public string? MatchedTerm { get; set; }
/// <summary>
/// Gets or sets the index number.

View File

@@ -398,7 +398,8 @@ namespace MediaBrowser.Providers.Manager
foreach (var child in children)
{
if (!child.IsFolder)
// Exclude any folders and virtual items since they are only placeholders
if (!child.IsFolder && !child.IsVirtualItem)
{
var childDateCreated = child.DateCreated;
if (childDateCreated > dateLastMediaAdded)

View File

@@ -1,6 +1,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
@@ -141,19 +142,15 @@ namespace MediaBrowser.Providers.MediaInfo
&& item.SupportsLocalMetadata
&& !video.IsPlaceHolder)
{
if (!video.SubtitleFiles.SequenceEqual(
_subtitleResolver.GetExternalFiles(video, directoryService, false)
.Select(info => info.Path).ToList(),
StringComparer.Ordinal))
var externalFiles = new HashSet<string>(_subtitleResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
if (!new HashSet<string>(video.SubtitleFiles, StringComparer.Ordinal).SetEquals(externalFiles))
{
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
return true;
}
if (!video.AudioFiles.SequenceEqual(
_audioResolver.GetExternalFiles(video, directoryService, false)
.Select(info => info.Path).ToList(),
StringComparer.Ordinal))
externalFiles = new HashSet<string>(_audioResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
if (!new HashSet<string>(video.AudioFiles, StringComparer.Ordinal).SetEquals(externalFiles))
{
_logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
return true;
@@ -161,14 +158,14 @@ namespace MediaBrowser.Providers.MediaInfo
}
if (item is Audio audio
&& item.SupportsLocalMetadata
&& !audio.LyricFiles.SequenceEqual(
_lyricResolver.GetExternalFiles(audio, directoryService, false)
.Select(info => info.Path).ToList(),
StringComparer.Ordinal))
&& item.SupportsLocalMetadata)
{
_logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path);
return true;
var externalFiles = new HashSet<string>(_lyricResolver.GetExternalFiles(audio, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
if (!new HashSet<string>(audio.LyricFiles, StringComparer.Ordinal).SetEquals(externalFiles))
{
_logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path);
return true;
}
}
return false;

View File

@@ -101,7 +101,7 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>,
bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
bool replace = options.ReplaceAllImages;
if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false))
if (!enableDuringScan.GetValueOrDefault(false))
{
return ItemUpdateType.None;
}

View File

@@ -1,4 +1,4 @@
using System.Reflection;
[assembly: AssemblyVersion("10.9.2")]
[assembly: AssemblyFileVersion("10.9.2")]
[assembly: AssemblyVersion("10.9.3")]
[assembly: AssemblyFileVersion("10.9.3")]

View File

@@ -15,7 +15,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId>
<VersionPrefix>10.9.2</VersionPrefix>
<VersionPrefix>10.9.3</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -30,25 +30,8 @@ namespace Jellyfin.LiveTv.TunerHosts
{
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
private static readonly string[] _disallowedMimeTypes =
{
"text/plain",
"text/html",
"video/x-matroska",
"video/mp4",
"application/vnd.apple.mpegurl",
"application/mpegurl",
"application/x-mpegurl",
"video/vnd.mpeg.dash.mpd"
};
private static readonly string[] _disallowedSharedStreamExtensions =
{
".mkv",
".mp4",
".m3u8",
".mpd"
};
private static readonly string[] _mimeTypesCanShareHttpStream = ["video/MP2T"];
private static readonly string[] _extensionsCanShareHttpStream = [".ts", ".tsv", ".m2t"];
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
@@ -113,27 +96,33 @@ namespace Jellyfin.LiveTv.TunerHosts
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
{
using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(message, cancellationToken)
.ConfigureAwait(false);
var extension = Path.GetExtension(new UriBuilder(mediaSource.Path).Path);
if (response.IsSuccessStatusCode)
if (string.IsNullOrEmpty(extension))
{
if (!_disallowedMimeTypes.Contains(response.Content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase))
try
{
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(message, cancellationToken)
.ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
if (_mimeTypesCanShareHttpStream.Contains(response.Content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase))
{
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
}
}
}
catch (Exception)
{
Logger.LogWarning("HEAD request to check MIME type failed, shared stream disabled");
}
}
else
else if (_extensionsCanShareHttpStream.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// Fallback to check path extension when the server does not support HEAD method
// Use UriBuilder to remove all query string as GetExtension will include them when used directly
var extension = Path.GetExtension(new UriBuilder(mediaSource.Path).Path);
if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
}
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
}
}

View File

@@ -903,6 +903,17 @@ public class NetworkManager : INetworkManager, IDisposable
return false;
}
/// <summary>
/// Get if the IPAddress is Link-local.
/// </summary>
/// <param name="address">The IP Address.</param>
/// <returns>Bool indicates if the address is link-local.</returns>
public bool IsLinkLocalAddress(IPAddress address)
{
ArgumentNullException.ThrowIfNull(address);
return NetworkConstants.IPv4RFC3927LinkLocal.Contains(address) || address.IsIPv6LinkLocal;
}
/// <inheritdoc/>
public bool IsInLocalNetwork(IPAddress address)
{
@@ -1084,7 +1095,11 @@ public class NetworkManager : INetworkManager, IDisposable
private bool MatchesExternalInterface(IPAddress source, out string result)
{
// Get the first external interface address that isn't a loopback.
var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)).OrderBy(x => x.Index).ToArray();
var extResult = _interfaces
.Where(p => !IsInLocalNetwork(p.Address))
.Where(p => p.Address.AddressFamily.Equals(source.AddressFamily))
.Where(p => !IsLinkLocalAddress(p.Address))
.OrderBy(x => x.Index).ToArray();
// No external interface found
if (extResult.Length == 0)