Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal] Implement DASH Thumbnail tracks #1496

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions demo/scripts/components/ThumbnailPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import * as React from "react";
import useModuleState from "../lib/useModuleState";
import { IPlayerModule } from "../modules/player";
import { IThumbnailMetadata } from "../../../src/public_types";

const DIV_SPINNER_STYLE = {
backgroundColor: "gray",
position: "absolute",
width: "100%",
height: "100%",
opacity: "50%",
display: "flex",
justifyContent: "center",
alignItems: "center",
} as const;

const IMG_SPINNER_STYLE = {
width: "50%",
margin: "auto",
} as const;

export default function ThumbnailPreview({
xPosition,
time,
player,
showVideoThumbnail,
}: {
player: IPlayerModule;
xPosition: number | null;
time: number;
showVideoThumbnail: boolean;
}): JSX.Element {
const videoThumbnailLoader = useModuleState(player, "videoThumbnailLoader");
const videoElement = useModuleState(player, "videoThumbnailsElement");
const imageThumbnailElement = useModuleState(player, "imageThumbnailContainerElement");
const parentElementRef = React.useRef<HTMLDivElement>(null);
const [shouldDisplaySpinner, setShouldDisplaySpinner] = React.useState(true);
const ceiledTime = Math.ceil(time);

// Insert the div element containing the image thumbnail
React.useEffect(() => {
if (showVideoThumbnail) {
return;
}

if (parentElementRef.current !== null) {
parentElementRef.current.appendChild(imageThumbnailElement);
}
return () => {
if (
parentElementRef.current !== null &&
parentElementRef.current.contains(imageThumbnailElement)
) {
parentElementRef.current.removeChild(imageThumbnailElement);
}
};
}, [showVideoThumbnail]);

// OR insert the video element containing the thumbnail
React.useEffect(() => {
if (!showVideoThumbnail) {
return;
}
if (videoElement !== null && parentElementRef.current !== null) {
parentElementRef.current.appendChild(videoElement);
}
return () => {
if (
videoElement !== null &&
parentElementRef.current !== null &&
parentElementRef.current.contains(videoElement)
) {
parentElementRef.current.removeChild(videoElement);
}
};
}, [videoElement, showVideoThumbnail]);

React.useEffect(() => {
if (!showVideoThumbnail) {
return;
}
player.actions.attachVideoThumbnailLoader();
return () => {
player.actions.dettachVideoThumbnailLoader();
};
}, [showVideoThumbnail]);

// Change the thumbnail when a new time is wanted
React.useEffect(() => {
let spinnerTimeout: number | null = null;
let loadThumbnailTimeout: number | null = null;

startSpinnerTimeoutIfNotAlreadyStarted();

// load thumbnail after a 40ms timer to avoid doing too many requests
// when the user quickly moves its pointer or whatever is calling this
loadThumbnailTimeout = window.setTimeout(() => {
loadThumbnailTimeout = null;
if (showVideoThumbnail) {
if (videoThumbnailLoader === null) {
return;
}
videoThumbnailLoader
.setTime(ceiledTime)
.then(hideSpinner)
.catch((err) => {
if (
typeof err === "object" &&
err !== null &&
(err as Partial<Record<string, unknown>>).code === "ABORTED"
) {
return;
} else {
hideSpinner();

// eslint-disable-next-line no-console
console.error("Error while loading thumbnails:", err);
}
});
} else {
const metadata = player.actions.getThumbnailMetadata(ceiledTime);
const thumbnailTrack = metadata.reduce((acc: IThumbnailMetadata | null, t) => {
if (acc === null || acc.height === undefined) {
return t;
}
if (t.height === undefined) {
return acc;
}
if (acc.height > t.height) {
return t.height > 100 ? t : acc;
} else {
return acc.height > 100 ? acc : t;
}
}, null);
if (thumbnailTrack === null) {
hideSpinner();
return;
}
player.actions
.renderThumbnail(ceiledTime, thumbnailTrack.id)
.then(hideSpinner)
.catch((err) => {
if (
typeof err === "object" &&
err !== null &&
(err as Partial<Record<string, unknown>>).code === "ABORTED"
) {
return;
} else {
hideSpinner();
// eslint-disable-next-line no-console
console.warn("Error while loading thumbnails:", err);
}
});
}
}, 30);

return () => {
if (loadThumbnailTimeout !== null) {
clearTimeout(loadThumbnailTimeout);
}
hideSpinner();
};

/**
* Display a spinner after some delay if `stopSpinnerTimeout` hasn't been
* called since.
* This function allows to schedule a spinner if the request to display a
* thumbnail takes too much time.
*/
function startSpinnerTimeoutIfNotAlreadyStarted() {
if (spinnerTimeout !== null) {
return;
}

// Wait a little before displaying spinner, to
// be sure loading takes time
spinnerTimeout = window.setTimeout(() => {
spinnerTimeout = null;
setShouldDisplaySpinner(true);
}, 150);
}

/**
* Hide the spinner if one is active and stop the last started spinner
* timeout.
* Allow to avoid showing a spinner when the thumbnail we were waiting for
* was succesfully loaded.
*/
function hideSpinner() {
if (spinnerTimeout !== null) {
clearTimeout(spinnerTimeout);
spinnerTimeout = null;
}
setShouldDisplaySpinner(false);
}
}, [ceiledTime, videoThumbnailLoader, parentElementRef]);

return (
<div
className="thumbnail-wrapper"
style={xPosition !== null ? { transform: `translate(${xPosition}px, -136px)` } : {}}
ref={parentElementRef}
>
{shouldDisplaySpinner ? (
<div style={DIV_SPINNER_STYLE}>
<img src="./assets/spinner.gif" style={IMG_SPINNER_STYLE} />
</div>
) : null}
</div>
);
}
152 changes: 0 additions & 152 deletions demo/scripts/components/VideoThumbnail.tsx

This file was deleted.

12 changes: 12 additions & 0 deletions demo/scripts/contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const DEFAULT_CONTENTS: IDefaultContent[] = [
transport: "dash",
live: false,
},
{
name: "Live with thumbnail track",
url: "https://livesim2.dashif.org/livesim2/testpic_2s/Manifest_thumbs.mpd",
transport: "dash",
live: true,
},
{
name: "Axinom CMAF multiple Audio and Text tracks Tears of steel",
url: "https://media.axprod.net/TestVectors/Cmaf/clear_1080p_h264/manifest.mpd",
Expand Down Expand Up @@ -64,6 +70,12 @@ const DEFAULT_CONTENTS: IDefaultContent[] = [
transport: "dash",
live: true,
},
{
name: "VOD with thumbnail track",
url: "https://dash.akamaized.net/akamai/bbb_30fps/bbb_with_tiled_thumbnails.mpd",
transport: "dash",
live: false,
},
{
name: "Super SpeedWay",
url: "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest",
Expand Down
Loading
Loading