diff --git a/src/main/ThumbnailGeneration.tsx b/src/main/ThumbnailGeneration.tsx
index 3a31768c8..33abd4bae 100644
--- a/src/main/ThumbnailGeneration.tsx
+++ b/src/main/ThumbnailGeneration.tsx
@@ -22,6 +22,7 @@ import {
setJumpTriggered,
setAspectRatio,
selectPrimaryThumbnailTrack,
+ setThumbnailTime,
} from "../redux/videoSlice";
import { Track } from "../types";
import Timeline from "./Timeline";
@@ -210,8 +211,10 @@ const ThumbnailActions: React.FC<{
// *track: Generate to
// *index: Generate from
const generate = (track: Track, index: number) => {
+ const time = generateRefs.current[index]?.getCurrentTime();
const uri = generateRefs.current[index]?.captureVideo();
dispatch(setThumbnail({ id: track.id, uri: uri }));
+ dispatch(setThumbnailTime({ id: track.id, time: time?.toString() }));
dispatch(setHasChanges(true));
};
diff --git a/src/main/ThumbnailSelect.tsx b/src/main/ThumbnailSelect.tsx
index 857a927fd..fbecbec89 100644
--- a/src/main/ThumbnailSelect.tsx
+++ b/src/main/ThumbnailSelect.tsx
@@ -1,7 +1,7 @@
import { css, SerializedStyles } from "@emotion/react";
import { IconType } from "react-icons";
import { LuCamera, LuCopy, LuCircleX, LuUpload } from "react-icons/lu";
-import React from "react";
+import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "../redux/store";
import {
@@ -16,11 +16,13 @@ import {
setHasChanges,
setThumbnail,
setThumbnails,
+ setThumbnailTime,
} from "../redux/videoSlice";
import { Track } from "../types";
import { ThemedTooltip } from "./Tooltip";
import { ProtoButton } from "@opencast/appkit";
import { setIndex, setIsDisplayEditView } from "../redux/thumbnailSlice";
+import ReactPlayer from "react-player";
/**
* Choose between various thumbnail actions for the available tracks.
@@ -98,6 +100,7 @@ const ThumbnailSelector: React.FC<{
track={track}
trackIndex={trackIndex}
/>
+
);
};
@@ -265,6 +268,7 @@ export const UploadButton: React.FC<{
if (e.target && e.target.result) {
const uri = e.target.result as string; // We know this must be string because we use "readAsDataURL"
dispatch(setThumbnail({ id: track.id, uri: uri }));
+ dispatch(setThumbnailTime({ id: track.id, time: undefined }));
dispatch(setHasChanges(true));
}
};
@@ -454,6 +458,63 @@ export const ThumbnailButton: React.FC<{
);
};
+/**
+ * Generates a temporary thumbnail from a timestamp
+ *
+ * Workaround for the backend being unable to send us thumbnails from
+ * publications. This way, we can at least show a thumbnail to a user
+ * if they previously generated one via timestamp.
+ */
+const WorkaroundThumbnailGenerator: React.FC<{
+ track: Track,
+}> = ({ track }) => {
+ const dispatch = useAppDispatch();
+
+ const ref = useRef(null);
+ const [ready, setReady] = useState(false);
+ const [seeked, setSeeked] = useState(false);
+
+ useEffect(() => {
+ if (ref.current && ready && track && track.thumbnailTime && !track.thumbnailUri) {
+ ref.current.seekTo(parseFloat(track.thumbnailTime), "seconds");
+ }
+ }, [dispatch, ready, track]);
+
+ useEffect(() => {
+ if (seeked) {
+ const videoElement = ref.current?.getInternalPlayer() as HTMLVideoElement;
+ const canvas = document.createElement("canvas");
+ canvas.width = videoElement.videoWidth;
+ canvas.height = videoElement.videoHeight;
+ const canvasContext = canvas.getContext("2d");
+ if (canvasContext !== null) {
+ canvasContext.drawImage(videoElement, 0, 0);
+ const uri = canvas.toDataURL("image/png");
+
+ if (uri) {
+ dispatch(setThumbnail({ id: track.id, uri: uri }));
+ }
+ }
+ }
+ }, [dispatch, seeked, track]);
+
+ const playerStyle = css({
+ display: "none",
+ });
+
+ return (
+ setReady(true)}
+ onSeek={() => setSeeked(true)}
+ />
+ );
+};
+
/**
* Shared CSS
*/
diff --git a/src/main/VideoPlayers.tsx b/src/main/VideoPlayers.tsx
index d5eee634e..83ec2bba6 100644
--- a/src/main/VideoPlayers.tsx
+++ b/src/main/VideoPlayers.tsx
@@ -116,6 +116,7 @@ const VideoPlayers: React.FC<{
export interface VideoPlayerForwardRef {
captureVideo: () => string | undefined,
getWidth: () => number,
+ getCurrentTime: () => number,
}
interface VideoPlayerProps {
@@ -390,6 +391,9 @@ export const VideoPlayer = React.forwardRef) => {
+ setThumbnailTimeHelper(state, action.payload.id, action.payload.time);
+ },
removeThumbnail: (state, action: PayloadAction) => {
const index = state.tracks.findIndex(t => t.id === action.payload);
state.tracks[index].thumbnailUri = undefined;
@@ -573,6 +576,13 @@ const setThumbnailHelper = (state: video, id: Track["id"], uri: Track["thumbnail
}
};
+const setThumbnailTimeHelper = (state: video, id: Track["id"], time: Track["thumbnailTime"]) => {
+ const index = state.tracks.findIndex(t => t.id === id);
+ if (index >= 0) {
+ state.tracks[index].thumbnailTime = time;
+ }
+};
+
export const {
addSegment,
cut,
@@ -603,6 +613,7 @@ export const {
setSelectedWorkflowIndex,
setThumbnail,
setThumbnails,
+ setThumbnailTime,
setVideoEnabled,
setVolume,
setWaveformImages,
diff --git a/src/types.ts b/src/types.ts
index ef3f9fa7c..1230dc4e2 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -16,6 +16,7 @@ export interface Track {
video_stream: {available: boolean, enabled: boolean, thumbnail_uri: string},
thumbnailUri: string | undefined,
thumbnailPriority: number,
+ thumbnailTime?: string,
}
export interface Flavor {