p2p-media-loader-core
Version:
P2P Media Loader core functionality
207 lines (184 loc) • 5.82 kB
text/typescript
import {
SegmentWithStream,
Stream,
StreamConfig,
StreamWithSegments,
} from "../types.js";
import { Playback } from "../internal-types.js";
import { P2PLoader } from "../p2p/loader.js";
import { sha1 } from "./hash.js";
export type SegmentPlaybackStatuses = {
isHighDemand: boolean;
isHttpDownloadable: boolean;
isP2PDownloadable: boolean;
};
export type PlaybackTimeWindowsConfig = Pick<
StreamConfig,
"highDemandTimeWindow" | "httpDownloadTimeWindow" | "p2pDownloadTimeWindow"
>;
const PEER_PROTOCOL_VERSION = "v2";
export type GenerateStreamShortIdProps = {
bitrate?: number | null;
codecs?: string | null;
width?: number | null;
height?: number | null;
language?: string | null;
channels?: string | number | null;
name?: string | null;
frameRate?: number | string | null;
videoRange?: string | null;
};
/**
* Generates a stable, unique string ID for a stream based on its properties.
* Uses a SHA-1 hash to avoid collisions and encodes the result to standard Base64.
*/
export function generateStreamShortId({
bitrate,
codecs,
width,
height,
language,
channels,
name,
frameRate,
videoRange,
}: GenerateStreamShortIdProps): string {
const normalizedCodecs = codecs
? codecs
.split(",")
.map((c: string) => {
c = c.trim().toLowerCase();
// Normalize decimal RFC 4281 avc1 codecs to hex (e.g., avc1.66.30 -> avc1.42001e)
const parts = c.split(".");
if (
parts.length === 3 &&
(parts[0] === "avc1" || parts[0] === "avc")
) {
const profile = parseInt(parts[1], 10);
const level = parseInt(parts[2], 10);
if (
!isNaN(profile) &&
!isNaN(level) &&
parts[1] === profile.toString() &&
parts[2] === level.toString()
) {
const profileHex = `00${profile.toString(16)}`.slice(-2);
const levelHex = `00${level.toString(16)}`.slice(-2);
c = `${parts[0]}.${profileHex}00${levelHex}`;
}
}
return c;
})
.sort()
.join(",")
: "";
const normalizedLanguage =
language && language !== "und" ? language.slice(0, 2).toLowerCase() : "";
const normalizedChannels = channels ? channels.toString().split("/")[0] : "";
const normalizedName = name ? name.toLowerCase().trim() : "";
// Normalize frame rates to eliminate trailing zeros (e.g. "30.000" -> "30")
const normalizedFrameRate =
frameRate && !isNaN(Number(frameRate)) ? Number(frameRate).toString() : "";
const normalizedVideoRange = videoRange
? videoRange.toUpperCase().trim()
: "";
const str = `${bitrate ?? 0}-${normalizedCodecs}-${width ?? ""}-${height ?? ""}-${normalizedLanguage}-${normalizedChannels}-${normalizedName}-${normalizedFrameRate}-${normalizedVideoRange}`;
return btoa(sha1(str));
}
export function getStreamSwarmId(
swarmId: string,
stream: Readonly<Stream>,
): string {
return `${PEER_PROTOCOL_VERSION}-${swarmId}-${getStreamId(stream)}`;
}
export function getSegmentFromStreamsMap(
streams: Map<string, StreamWithSegments>,
segmentRuntimeId: string,
): SegmentWithStream | undefined {
for (const stream of streams.values()) {
const segment = stream.segments.get(segmentRuntimeId);
if (segment) return segment;
}
}
export function getSegmentFromStreamByExternalId(
stream: StreamWithSegments,
segmentExternalId: number,
): SegmentWithStream | undefined {
for (const segment of stream.segments.values()) {
if (segment.externalId === segmentExternalId) return segment;
}
}
export function getStreamId(stream: Stream) {
return `${stream.type}-${stream.index}`;
}
export function getSegmentAvgDuration(stream: StreamWithSegments) {
const { segments } = stream;
let sumDuration = 0;
const { size } = segments;
if (size === 0) return 0;
for (const segment of segments.values()) {
const duration = segment.endTime - segment.startTime;
sumDuration += duration;
}
return sumDuration / size;
}
function calculateTimeWindows(
timeWindowsConfig: PlaybackTimeWindowsConfig,
availableMemoryInPercent: number,
) {
const {
highDemandTimeWindow,
httpDownloadTimeWindow,
p2pDownloadTimeWindow,
} = timeWindowsConfig;
const result = {
highDemandTimeWindow,
httpDownloadTimeWindow,
p2pDownloadTimeWindow,
};
if (availableMemoryInPercent <= 5) {
result.httpDownloadTimeWindow = 0;
result.p2pDownloadTimeWindow = 0;
} else if (availableMemoryInPercent <= 10) {
result.p2pDownloadTimeWindow = result.httpDownloadTimeWindow;
}
return result;
}
export function getSegmentPlaybackStatuses(
segment: SegmentWithStream,
playback: Playback,
timeWindowsConfig: PlaybackTimeWindowsConfig,
currentP2PLoader: P2PLoader,
availableMemoryPercent: number,
): SegmentPlaybackStatuses {
const {
highDemandTimeWindow,
httpDownloadTimeWindow,
p2pDownloadTimeWindow,
} = calculateTimeWindows(timeWindowsConfig, availableMemoryPercent);
return {
isHighDemand: isSegmentInTimeWindow(
segment,
playback,
highDemandTimeWindow,
),
isHttpDownloadable: isSegmentInTimeWindow(
segment,
playback,
httpDownloadTimeWindow,
),
isP2PDownloadable:
isSegmentInTimeWindow(segment, playback, p2pDownloadTimeWindow) &&
currentP2PLoader.isSegmentLoadingOrLoadedBySomeone(segment),
};
}
function isSegmentInTimeWindow(
segment: SegmentWithStream,
playback: Playback,
timeWindowLength: number,
) {
const { startTime, endTime } = segment;
const { position, rate } = playback;
const rightMargin = position + timeWindowLength * rate;
return !(rightMargin < startTime || position > endTime);
}