mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
fix(playback): preserve distinct recovery fallbacks
This commit is contained in:
1 parent
0dd8c1a57d
commit
eb9ae0de18
9 files changed
+325
-110
No files matched your search
@@ -1008,9 +1008,11 @@ player in settings.
|
||||
unknown stage/failure. Public DASH text-parser codes likewise retain their
|
||||
exact `TEXT` category/code but keep stage/failure unknown. A failed
|
||||
`Player.isBrowserSupported()` preflight also stays unknown rather than
|
||||
claiming container incompatibility; clear DASH still offers configured
|
||||
external-player actions, while KODIPROP DRM keeps them disabled because
|
||||
those players never receive its keys.
|
||||
claiming container incompatibility. It carries only the enumerated,
|
||||
app-owned `PlaybackRuntimeSupport.ShakaBrowserUnsupported` marker — never a
|
||||
producer recommendation hint or engine payload. Clear DASH still offers
|
||||
configured external-player actions, while KODIPROP DRM keeps them disabled
|
||||
because those players never receive its keys.
|
||||
Channels with `drm.supported === false` emit a `DrmOrEncryption` diagnostic
|
||||
with a fixed safe detail string and without starting an engine.
|
||||
- HTML5 player: `extension === 'mpd'` branch in `playChannel()`. ArtPlayer:
|
||||
|
||||
@@ -32,6 +32,13 @@ export const PlaybackDiagnosticSource = {
|
||||
export type PlaybackDiagnosticSource =
|
||||
(typeof PlaybackDiagnosticSource)[keyof typeof PlaybackDiagnosticSource];
|
||||
|
||||
export const PlaybackRuntimeSupport = {
|
||||
ShakaBrowserUnsupported: 'shaka-browser-unsupported',
|
||||
} as const;
|
||||
|
||||
export type PlaybackRuntimeSupport =
|
||||
(typeof PlaybackRuntimeSupport)[keyof typeof PlaybackRuntimeSupport];
|
||||
|
||||
export const InlinePlaybackPlayer = {
|
||||
VideoJs: 'videojs',
|
||||
Html5: 'html5',
|
||||
@@ -255,6 +262,7 @@ export interface PlaybackDiagnostic {
|
||||
readonly player?: InlinePlaybackPlayer;
|
||||
readonly audioCodecs: readonly string[];
|
||||
readonly videoCodecs: readonly string[];
|
||||
readonly runtimeSupport?: PlaybackRuntimeSupport;
|
||||
readonly details?: string;
|
||||
readonly nativeErrorCode?: number;
|
||||
readonly nativeErrorMessage?: string;
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
PlaybackDiagnostic,
|
||||
PlaybackDiagnosticCode,
|
||||
PlaybackDiagnosticSource,
|
||||
PlaybackRuntimeSupport,
|
||||
PlaybackSourceMetadata,
|
||||
ShakaPlaybackEvidence,
|
||||
VhsPlaybackEngineType as VhsPlaybackEngineTypeValue,
|
||||
@@ -200,6 +201,7 @@ export function createPlaybackDiagnostic(options: {
|
||||
readonly code: PlaybackDiagnosticCode;
|
||||
readonly source: PlaybackDiagnosticSource;
|
||||
readonly metadata: PlaybackSourceMetadata;
|
||||
readonly runtimeSupport?: PlaybackRuntimeSupport;
|
||||
readonly details?: string;
|
||||
readonly nativeErrorCode?: number;
|
||||
readonly nativeErrorMessage?: string;
|
||||
@@ -214,6 +216,7 @@ export function createPlaybackDiagnostic(options: {
|
||||
code,
|
||||
source,
|
||||
metadata,
|
||||
runtimeSupport,
|
||||
details,
|
||||
nativeErrorCode,
|
||||
nativeErrorMessage,
|
||||
@@ -234,6 +237,7 @@ export function createPlaybackDiagnostic(options: {
|
||||
player: metadata.player,
|
||||
audioCodecs: metadata.audioCodecs,
|
||||
videoCodecs: metadata.videoCodecs,
|
||||
runtimeSupport,
|
||||
details: details || undefined,
|
||||
nativeErrorCode,
|
||||
nativeErrorMessage,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ShakaPlaybackCategory,
|
||||
ShakaPlaybackDisposition as ShakaDisposition,
|
||||
ShakaPlaybackFailure,
|
||||
PlaybackRuntimeSupport,
|
||||
} from './playback-diagnostics.model';
|
||||
import { createPlaybackDiagnostic } from './playback-diagnostics.util';
|
||||
import { SHAKA_ERROR_CODE } from './shaka-error-contract';
|
||||
@@ -91,6 +92,19 @@ export function createUnsupportedDrmDiagnostic(
|
||||
});
|
||||
}
|
||||
|
||||
/** Exact app-owned evidence for Shaka's browser-support preflight. */
|
||||
export function createShakaBrowserUnsupportedDiagnostic(
|
||||
metadata: PlaybackSourceMetadata
|
||||
): PlaybackDiagnostic {
|
||||
return createPlaybackDiagnostic({
|
||||
code: DiagnosticCode.UnknownPlaybackError,
|
||||
source: DiagnosticSource.Shaka,
|
||||
metadata,
|
||||
runtimeSupport: PlaybackRuntimeSupport.ShakaBrowserUnsupported,
|
||||
shaka: createShakaPlaybackEvidence(null, ShakaDisposition.Terminal),
|
||||
});
|
||||
}
|
||||
|
||||
/** Narrows an unknown rejection to a Shaka-error-like shape, if it is one. */
|
||||
export function asShakaError(error: unknown): Partial<ShakaErrorLike> | null {
|
||||
if (!error || typeof error !== 'object') {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { InlinePlaybackPlayer } from './diagnostics/playback-diagnostics.model';
|
||||
import {
|
||||
PlaybackEngineFamily,
|
||||
type PlaybackRecommendationContext,
|
||||
type PlaybackRecommendationTarget,
|
||||
type PlaybackTargetCapability,
|
||||
} from './playback-recommendation.model';
|
||||
import { getInlinePlaybackEngineFamily } from './playback-target-capabilities';
|
||||
|
||||
interface RuntimeCapabilityRecord {
|
||||
readonly target: unknown;
|
||||
readonly kind: unknown;
|
||||
readonly available: unknown;
|
||||
readonly engineFamily?: unknown;
|
||||
}
|
||||
|
||||
export type PlaybackCapabilityIndex = ReadonlyMap<
|
||||
PlaybackRecommendationTarget,
|
||||
PlaybackTargetCapability
|
||||
>;
|
||||
|
||||
const CANONICAL_TARGETS: readonly PlaybackRecommendationTarget[] = [
|
||||
InlinePlaybackPlayer.VideoJs,
|
||||
InlinePlaybackPlayer.Html5,
|
||||
InlinePlaybackPlayer.ArtPlayer,
|
||||
'mpv',
|
||||
'vlc',
|
||||
];
|
||||
|
||||
export function createPlaybackCapabilityIndex(
|
||||
context: PlaybackRecommendationContext
|
||||
): PlaybackCapabilityIndex | null {
|
||||
const index = new Map<
|
||||
PlaybackRecommendationTarget,
|
||||
PlaybackTargetCapability
|
||||
>();
|
||||
for (const capability of context.targetCapabilities) {
|
||||
if (!isValidCapability(context, capability, index)) {
|
||||
return null;
|
||||
}
|
||||
index.set(capability.target, capability);
|
||||
}
|
||||
if (index.size !== CANONICAL_TARGETS.length) {
|
||||
return null;
|
||||
}
|
||||
const active = index.get(context.activeTarget);
|
||||
return active?.kind === 'inline' &&
|
||||
active.available &&
|
||||
active.engineFamily !== null
|
||||
? index
|
||||
: null;
|
||||
}
|
||||
|
||||
export function hasAvailableCapability(
|
||||
capabilityIndex: PlaybackCapabilityIndex | null,
|
||||
target: PlaybackRecommendationTarget
|
||||
): boolean {
|
||||
return capabilityIndex?.get(target)?.available === true;
|
||||
}
|
||||
|
||||
export function getAttemptedInlineFamilies(
|
||||
context: PlaybackRecommendationContext,
|
||||
capabilityIndex: PlaybackCapabilityIndex | null
|
||||
): ReadonlySet<PlaybackEngineFamily> {
|
||||
const families = new Set<PlaybackEngineFamily>();
|
||||
if (capabilityIndex === null) {
|
||||
return families;
|
||||
}
|
||||
for (const target of context.attemptedTargets) {
|
||||
const capability = capabilityIndex.get(target);
|
||||
if (
|
||||
capability?.kind === 'inline' &&
|
||||
capability.available &&
|
||||
capability.engineFamily !== null
|
||||
) {
|
||||
families.add(capability.engineFamily);
|
||||
}
|
||||
}
|
||||
return families;
|
||||
}
|
||||
|
||||
export function isInlineFamilyAttempted(
|
||||
target: PlaybackRecommendationTarget,
|
||||
capabilityIndex: PlaybackCapabilityIndex | null,
|
||||
attemptedFamilies: ReadonlySet<PlaybackEngineFamily>
|
||||
): boolean {
|
||||
const capability = capabilityIndex?.get(target);
|
||||
return (
|
||||
capability?.kind === 'inline' &&
|
||||
capability.engineFamily !== null &&
|
||||
attemptedFamilies.has(capability.engineFamily)
|
||||
);
|
||||
}
|
||||
|
||||
function isValidCapability(
|
||||
context: PlaybackRecommendationContext,
|
||||
capability: unknown,
|
||||
index: PlaybackCapabilityIndex
|
||||
): capability is PlaybackTargetCapability {
|
||||
if (
|
||||
!hasRequiredCapabilityFields(capability) ||
|
||||
!isKnownTarget(capability.target) ||
|
||||
index.has(capability.target) ||
|
||||
typeof capability.available !== 'boolean'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!isInlineTarget(capability.target)) {
|
||||
return capability.kind === 'external';
|
||||
}
|
||||
if (capability.kind !== 'inline' || !('engineFamily' in capability)) {
|
||||
return false;
|
||||
}
|
||||
const expectedFamily = getInlinePlaybackEngineFamily(
|
||||
context.source.kind,
|
||||
capability.target
|
||||
);
|
||||
return (
|
||||
capability.engineFamily === expectedFamily &&
|
||||
!(expectedFamily === null && capability.available)
|
||||
);
|
||||
}
|
||||
|
||||
function hasRequiredCapabilityFields(
|
||||
value: unknown
|
||||
): value is RuntimeCapabilityRecord {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'target' in value &&
|
||||
'kind' in value &&
|
||||
'available' in value
|
||||
);
|
||||
}
|
||||
|
||||
function isKnownTarget(value: unknown): value is PlaybackRecommendationTarget {
|
||||
return CANONICAL_TARGETS.includes(value as PlaybackRecommendationTarget);
|
||||
}
|
||||
|
||||
function isInlineTarget(
|
||||
value: PlaybackRecommendationTarget
|
||||
): value is InlinePlaybackPlayer {
|
||||
return value !== 'mpv' && value !== 'vlc';
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { recommendPlaybackRecovery } from './playback-recommendation-policy';
|
||||
import { createPlaybackTargetCapabilities } from './playback-target-capabilities';
|
||||
|
||||
interface ContextOptions {
|
||||
readonly diagnostic?: PlaybackDiagnostic;
|
||||
readonly code?: PlaybackDiagnosticCode;
|
||||
readonly sourceKind?: PlaybackSourceKind;
|
||||
readonly activeTarget?: PlaybackRecommendationTarget;
|
||||
@@ -43,9 +44,11 @@ function diagnostic(code: PlaybackDiagnosticCode): PlaybackDiagnostic {
|
||||
function context(options: ContextOptions = {}): PlaybackRecommendationContext {
|
||||
const sourceKind = options.sourceKind ?? PlaybackSourceKind.Hls;
|
||||
return {
|
||||
diagnostic: diagnostic(
|
||||
options.code ?? PlaybackDiagnosticCode.UnknownPlaybackError
|
||||
),
|
||||
diagnostic:
|
||||
options.diagnostic ??
|
||||
diagnostic(
|
||||
options.code ?? PlaybackDiagnosticCode.UnknownPlaybackError
|
||||
),
|
||||
activeTarget: options.activeTarget ?? InlinePlaybackPlayer.VideoJs,
|
||||
attemptedTargets: options.attemptedTargets ?? new Set(),
|
||||
targetCapabilities:
|
||||
@@ -65,6 +68,16 @@ function context(options: ContextOptions = {}): PlaybackRecommendationContext {
|
||||
};
|
||||
}
|
||||
|
||||
function unsupportedShakaBrowserDiagnostic(): PlaybackDiagnostic {
|
||||
return {
|
||||
...diagnostic(PlaybackDiagnosticCode.UnknownPlaybackError),
|
||||
source: PlaybackDiagnosticSource.Shaka,
|
||||
container: 'mpd',
|
||||
mimeType: 'application/dash+xml',
|
||||
runtimeSupport: 'shaka-browser-unsupported',
|
||||
};
|
||||
}
|
||||
|
||||
function retry(
|
||||
reason: PlaybackRecommendationReason,
|
||||
priority: PlaybackRecommendationPriority = 'primary'
|
||||
@@ -450,6 +463,74 @@ describe('recommendPlaybackRecovery', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('excludes an inline family already attempted through a sibling target', () => {
|
||||
expect(
|
||||
recommendPlaybackRecovery(
|
||||
context({
|
||||
code: PlaybackDiagnosticCode.MediaDecodeError,
|
||||
activeTarget: InlinePlaybackPlayer.VideoJs,
|
||||
attemptedTargets: new Set([InlinePlaybackPlayer.ArtPlayer]),
|
||||
})
|
||||
)
|
||||
).toEqual([
|
||||
player(
|
||||
'mpv',
|
||||
PlaybackRecommendationReason.ExternalCodecOrContainerSupport,
|
||||
'primary'
|
||||
),
|
||||
player(
|
||||
'vlc',
|
||||
PlaybackRecommendationReason.ExternalCodecOrContainerSupport
|
||||
),
|
||||
alternative(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps managed external recovery for exact clear DASH browser-support evidence', () => {
|
||||
expect(
|
||||
recommendPlaybackRecovery(
|
||||
context({
|
||||
diagnostic: unsupportedShakaBrowserDiagnostic(),
|
||||
sourceKind: PlaybackSourceKind.Dash,
|
||||
activeTarget: InlinePlaybackPlayer.Html5,
|
||||
})
|
||||
)
|
||||
).toEqual([
|
||||
player(
|
||||
'mpv',
|
||||
PlaybackRecommendationReason.ExternalCodecOrContainerSupport,
|
||||
'primary'
|
||||
),
|
||||
player(
|
||||
'vlc',
|
||||
PlaybackRecommendationReason.ExternalCodecOrContainerSupport
|
||||
),
|
||||
alternative(),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'PWA runtime',
|
||||
options: { managedExternalPlayersAvailable: false },
|
||||
},
|
||||
{
|
||||
name: 'untransferable ClearKey DRM',
|
||||
options: { drm: 'untransferable' as const },
|
||||
},
|
||||
])('suppresses DASH browser-support fallback in $name', ({ options }) => {
|
||||
expect(
|
||||
recommendPlaybackRecovery(
|
||||
context({
|
||||
diagnostic: unsupportedShakaBrowserDiagnostic(),
|
||||
sourceKind: PlaybackSourceKind.Dash,
|
||||
activeTarget: InlinePlaybackPlayer.Html5,
|
||||
...options,
|
||||
})
|
||||
)
|
||||
).toEqual([alternative('primary')]);
|
||||
});
|
||||
|
||||
it('ignores capability array order when choosing canonical HTML5', () => {
|
||||
const targetCapabilities = [
|
||||
...createPlaybackTargetCapabilities({
|
||||
|
||||
@@ -2,16 +2,25 @@ import type { ExternalPlayerName } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
InlinePlaybackPlayer,
|
||||
PlaybackDiagnosticCode,
|
||||
PlaybackDiagnosticSource,
|
||||
PlaybackRuntimeSupport,
|
||||
} from './diagnostics/playback-diagnostics.model';
|
||||
import {
|
||||
PlaybackEngineFamily,
|
||||
PlaybackRecommendationReason,
|
||||
PlaybackSourceKind,
|
||||
type PlaybackRecommendation,
|
||||
type PlaybackRecommendationContext,
|
||||
type PlaybackRecommendationTarget,
|
||||
type PlaybackTargetCapability,
|
||||
} from './playback-recommendation.model';
|
||||
import { getInlinePlaybackEngineFamily } from './playback-target-capabilities';
|
||||
import {
|
||||
createPlaybackCapabilityIndex,
|
||||
getAttemptedInlineFamilies,
|
||||
hasAvailableCapability,
|
||||
isInlineFamilyAttempted,
|
||||
type PlaybackCapabilityIndex,
|
||||
} from './playback-recommendation-capabilities';
|
||||
|
||||
type WithoutPriority<T> = T extends unknown ? Omit<T, 'priority'> : never;
|
||||
|
||||
@@ -27,32 +36,16 @@ type AvailableInlineCapability = InlineCapability & {
|
||||
readonly engineFamily: PlaybackEngineFamily;
|
||||
};
|
||||
|
||||
interface RuntimeCapabilityRecord {
|
||||
readonly target: unknown;
|
||||
readonly kind: unknown;
|
||||
readonly available: unknown;
|
||||
readonly engineFamily?: unknown;
|
||||
}
|
||||
|
||||
type CapabilityIndex = ReadonlyMap<
|
||||
PlaybackRecommendationTarget,
|
||||
PlaybackTargetCapability
|
||||
>;
|
||||
|
||||
const CANONICAL_TARGETS: readonly PlaybackRecommendationTarget[] = [
|
||||
InlinePlaybackPlayer.VideoJs,
|
||||
InlinePlaybackPlayer.Html5,
|
||||
InlinePlaybackPlayer.ArtPlayer,
|
||||
'mpv',
|
||||
'vlc',
|
||||
];
|
||||
|
||||
export function recommendPlaybackRecovery(
|
||||
context: PlaybackRecommendationContext
|
||||
): readonly PlaybackRecommendation[] {
|
||||
const capabilityIndex = isPlayerOrientedDiagnostic(context.diagnostic.code)
|
||||
? createCapabilityIndex(context)
|
||||
const capabilityIndex = hasPlayerRecoveryEvidence(context)
|
||||
? createPlaybackCapabilityIndex(context)
|
||||
: null;
|
||||
const attemptedInlineFamilies = getAttemptedInlineFamilies(
|
||||
context,
|
||||
capabilityIndex
|
||||
);
|
||||
const seenTargets = new Set<PlaybackRecommendationTarget>();
|
||||
const candidates = buildCandidates(context, capabilityIndex).filter(
|
||||
isCandidate
|
||||
@@ -69,6 +62,15 @@ export function recommendPlaybackRecovery(
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
isInlineFamilyAttempted(
|
||||
candidate.target,
|
||||
capabilityIndex,
|
||||
attemptedInlineFamilies
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
isExternalTarget(candidate.target) &&
|
||||
!canTransferExternally(context)
|
||||
@@ -87,15 +89,20 @@ export function recommendPlaybackRecovery(
|
||||
|
||||
function buildCandidates(
|
||||
context: PlaybackRecommendationContext,
|
||||
capabilityIndex: CapabilityIndex | null
|
||||
capabilityIndex: PlaybackCapabilityIndex | null
|
||||
): readonly (PlaybackRecommendationCandidate | null)[] {
|
||||
if (
|
||||
isPlayerOrientedDiagnostic(context.diagnostic.code) &&
|
||||
capabilityIndex === null
|
||||
) {
|
||||
if (hasPlayerRecoveryEvidence(context) && capabilityIndex === null) {
|
||||
return [retryUnknown(), alternative(context)];
|
||||
}
|
||||
|
||||
if (isShakaBrowserUnsupported(context)) {
|
||||
return [
|
||||
externalCodecSupport('mpv'),
|
||||
externalCodecSupport('vlc'),
|
||||
alternative(context),
|
||||
];
|
||||
}
|
||||
|
||||
switch (context.diagnostic.code) {
|
||||
case PlaybackDiagnosticCode.NetworkError:
|
||||
return [retryTransient(), alternative(context)];
|
||||
@@ -174,7 +181,7 @@ function alternative(
|
||||
|
||||
function distinctInline(
|
||||
context: PlaybackRecommendationContext,
|
||||
capabilityIndex: CapabilityIndex | null,
|
||||
capabilityIndex: PlaybackCapabilityIndex | null,
|
||||
reason: PlaybackRecommendationCandidate['reason']
|
||||
): PlaybackRecommendationCandidate | null {
|
||||
const activeCapability = getActiveInlineCapability(
|
||||
@@ -213,7 +220,7 @@ function getCanonicalDistinctInlineTarget(
|
||||
|
||||
function getActiveInlineCapability(
|
||||
context: PlaybackRecommendationContext,
|
||||
capabilityIndex: CapabilityIndex | null
|
||||
capabilityIndex: PlaybackCapabilityIndex | null
|
||||
): AvailableInlineCapability | null {
|
||||
const capability = capabilityIndex?.get(context.activeTarget);
|
||||
return capability?.kind === 'inline' &&
|
||||
@@ -251,88 +258,28 @@ function isPlayerOrientedDiagnostic(
|
||||
);
|
||||
}
|
||||
|
||||
function hasAvailableCapability(
|
||||
capabilityIndex: CapabilityIndex | null,
|
||||
target: PlaybackRecommendationTarget
|
||||
): boolean {
|
||||
return capabilityIndex?.get(target)?.available === true;
|
||||
}
|
||||
|
||||
function createCapabilityIndex(
|
||||
function hasPlayerRecoveryEvidence(
|
||||
context: PlaybackRecommendationContext
|
||||
): CapabilityIndex | null {
|
||||
const index = new Map<
|
||||
PlaybackRecommendationTarget,
|
||||
PlaybackTargetCapability
|
||||
>();
|
||||
for (const capability of context.targetCapabilities) {
|
||||
if (!isValidCapability(context, capability, index)) {
|
||||
return null;
|
||||
}
|
||||
index.set(capability.target, capability);
|
||||
}
|
||||
if (index.size !== CANONICAL_TARGETS.length) {
|
||||
return null;
|
||||
}
|
||||
const active = index.get(context.activeTarget);
|
||||
return active?.kind === 'inline' &&
|
||||
active.available &&
|
||||
active.engineFamily !== null
|
||||
? index
|
||||
: null;
|
||||
}
|
||||
|
||||
function isValidCapability(
|
||||
context: PlaybackRecommendationContext,
|
||||
capability: unknown,
|
||||
index: CapabilityIndex
|
||||
): capability is PlaybackTargetCapability {
|
||||
if (
|
||||
!hasRequiredCapabilityFields(capability) ||
|
||||
!isKnownTarget(capability.target) ||
|
||||
index.has(capability.target) ||
|
||||
typeof capability.available !== 'boolean'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!isInlineTarget(capability.target)) {
|
||||
return capability.kind === 'external';
|
||||
}
|
||||
if (capability.kind !== 'inline' || !('engineFamily' in capability)) {
|
||||
return false;
|
||||
}
|
||||
const expectedFamily = getInlinePlaybackEngineFamily(
|
||||
context.source.kind,
|
||||
capability.target
|
||||
);
|
||||
): boolean {
|
||||
return (
|
||||
capability.engineFamily === expectedFamily &&
|
||||
!(expectedFamily === null && capability.available)
|
||||
isPlayerOrientedDiagnostic(context.diagnostic.code) ||
|
||||
isShakaBrowserUnsupported(context)
|
||||
);
|
||||
}
|
||||
|
||||
function hasRequiredCapabilityFields(
|
||||
value: unknown
|
||||
): value is RuntimeCapabilityRecord {
|
||||
function isShakaBrowserUnsupported(
|
||||
context: PlaybackRecommendationContext
|
||||
): boolean {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'target' in value &&
|
||||
'kind' in value &&
|
||||
'available' in value
|
||||
context.diagnostic.code ===
|
||||
PlaybackDiagnosticCode.UnknownPlaybackError &&
|
||||
context.diagnostic.source === PlaybackDiagnosticSource.Shaka &&
|
||||
context.diagnostic.runtimeSupport ===
|
||||
PlaybackRuntimeSupport.ShakaBrowserUnsupported &&
|
||||
context.source.kind === PlaybackSourceKind.Dash
|
||||
);
|
||||
}
|
||||
|
||||
function isKnownTarget(value: unknown): value is PlaybackRecommendationTarget {
|
||||
return CANONICAL_TARGETS.includes(value as PlaybackRecommendationTarget);
|
||||
}
|
||||
|
||||
function isInlineTarget(
|
||||
value: PlaybackRecommendationTarget
|
||||
): value is InlinePlaybackPlayer {
|
||||
return !isExternalTarget(value);
|
||||
}
|
||||
|
||||
function isExternalTarget(
|
||||
target: PlaybackRecommendationTarget
|
||||
): target is ExternalPlayerName {
|
||||
|
||||
@@ -398,6 +398,7 @@ describe('ShakaVideoSession', () => {
|
||||
});
|
||||
expect(JSON.stringify(issues[0])).not.toContain(secret);
|
||||
expect(JSON.stringify(issues[0])).not.toContain('provider.example');
|
||||
expect(issues[0].runtimeSupport).toBeUndefined();
|
||||
});
|
||||
|
||||
it('recovers from a stalled load: stop() interrupts it and the next start proceeds', async () => {
|
||||
@@ -508,6 +509,10 @@ describe('ShakaVideoSession', () => {
|
||||
stage: 'unknown',
|
||||
failure: 'unknown',
|
||||
});
|
||||
expect(issues[0]).toHaveProperty(
|
||||
'runtimeSupport',
|
||||
'shaka-browser-unsupported'
|
||||
);
|
||||
});
|
||||
|
||||
it('emits the same unknown diagnostic when browser support fails with DRM', async () => {
|
||||
@@ -526,6 +531,10 @@ describe('ShakaVideoSession', () => {
|
||||
expect(issues[0].code).toBe(
|
||||
PlaybackDiagnosticCode.UnknownPlaybackError
|
||||
);
|
||||
expect(issues[0]).toHaveProperty(
|
||||
'runtimeSupport',
|
||||
'shaka-browser-unsupported'
|
||||
);
|
||||
});
|
||||
|
||||
it('destroy tears down the engine and blocks later starts', async () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
asShakaError,
|
||||
classifyShakaPlaybackIssue,
|
||||
createPlaybackSourceMetadata,
|
||||
createShakaBrowserUnsupportedDiagnostic,
|
||||
createUnsupportedDrmDiagnostic,
|
||||
getShakaErrorEventDisposition,
|
||||
isShakaLoadInterrupted,
|
||||
@@ -139,7 +140,12 @@ export class ShakaVideoSession {
|
||||
}
|
||||
|
||||
if (!module.Player.isBrowserSupported()) {
|
||||
this.emitTerminalIfCurrent(generation, null, url);
|
||||
this.emitIfCurrent(
|
||||
generation,
|
||||
createShakaBrowserUnsupportedDiagnostic(
|
||||
this.createMetadata(url)
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user