Skip to content

Commit a7903d0

Browse files
committed
Make editor audio-only proof
Make sure the editor deals reasonably well with audio-only tracks. Includes: - Displaying a little image for the player if there is no video stream. - Make sure the track selection makes sense (don't display a video stream for an audio only track, don't allow users to deselect all tracks) - Disable thumbnail generation for audio-only tracks, but keep thumbnail generation.
1 parent 40f5cbd commit a7903d0

10 files changed

Lines changed: 66 additions & 49 deletions

File tree

src/i18n/locales/en-US.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,7 @@
233233
"unauthorizedError-text": "You are not allowed to edit this video",
234234
"comError-text": "A problem occurred during communication with Opencast.",
235235
"loadError-text": "An error has occurred loading this video.",
236-
"durationError-text": "Opencast failed to provide the video duration.",
237-
"noVideoError-text": "The editor does not support audio files yet!"
236+
"durationError-text": "Opencast failed to provide the video duration."
238237
},
239238

240239
"landing": {

src/img/video-off.png

818 Bytes
Loading

src/main/Cutting.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
setIsPlayPreview,
1919
jumpToPreviousSegment,
2020
jumpToNextSegment,
21-
selectVideos,
2221
cut,
2322
mergeAll,
2423
mergeLeft,
@@ -48,7 +47,6 @@ const Cutting: React.FC = () => {
4847
state.videoState.status);
4948
const error = useAppSelector((state: { videoState: { error: httpRequestState["error"]; }; }) =>
5049
state.videoState.error);
51-
const videos = useAppSelector(selectVideos);
5250
const duration = useAppSelector(selectDuration);
5351
const theme = useTheme();
5452
const errorReason = useAppSelector((state: { videoState: { errorReason: httpRequestState["errorReason"]; }; }) =>
@@ -75,14 +73,6 @@ const Cutting: React.FC = () => {
7573
}));
7674
}
7775
} else if (videoURLStatus === "success") {
78-
// Editor can not handle events with no videos/audio-only atm
79-
if (videos === null || videos.length === 0) {
80-
dispatch(setError({
81-
error: true,
82-
errorMessage: t("error.noVideoError-text"),
83-
errorDetails: error,
84-
}));
85-
}
8676
if (duration === null) {
8777
dispatch(setError({
8878
error: true,
@@ -91,7 +81,7 @@ const Cutting: React.FC = () => {
9181
}));
9282
}
9383
}
94-
}, [videoURLStatus, dispatch, error, t, errorReason, duration, videos]);
84+
}, [videoURLStatus, dispatch, error, t, errorReason, duration]);
9585

9686
// Already try fetching Metadata to reduce wait time
9787
useEffect(() => {

src/main/SubtitleVideoArea.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { css } from "@emotion/react";
33
import { RootState, ThunkApiConfig, useAppSelector } from "../redux/store";
44
import {
55
selectIsMuted,
6-
selectVideos,
6+
selectTracks,
77
selectVolume,
88
selectJumpTriggered,
99
setIsMuted,
@@ -63,7 +63,7 @@ const SubtitleVideoArea: React.FC<{
6363
setCurrentlyAtAndTriggerPreview,
6464
}) => {
6565

66-
const tracks = useAppSelector(selectVideos);
66+
const tracks = useAppSelector(selectTracks);
6767
const subtitle = useAppSelector(selectSelectedSubtitleById);
6868
const [selectedFlavor, setSelectedFlavor] = useState<Flavor>();
6969
const [subtitleUrl, setSubtitleUrl] = useState("");
@@ -103,6 +103,14 @@ const SubtitleVideoArea: React.FC<{
103103
}
104104
};
105105

106+
const isAudioOnly = () => {
107+
for (const track of tracks) {
108+
if (track.flavor.type === selectedFlavor?.type && track.flavor.subtype === selectedFlavor?.subtype) {
109+
return !track.video_stream.available;
110+
}
111+
}
112+
};
113+
106114
// Parse subtitles to something the video player understands
107115
useEffect(() => {
108116
if (subtitle?.cues) {
@@ -143,6 +151,7 @@ const SubtitleVideoArea: React.FC<{
143151
subtitleUrl={subtitleUrl}
144152
first={true}
145153
last={true}
154+
audioOnly={isAudioOnly()}
146155
selectIsPlaying={selectIsPlaying}
147156
selectIsMuted={selectIsMuted}
148157
selectCurrentlyAtInSeconds={selectCurrentlyAtInSeconds}

src/main/ThumbnailSelect.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
import { Theme, useTheme } from "../themes";
1212
import {
1313
selectOriginalThumbnails,
14-
selectVideos,
1514
selectTracks,
1615
setHasChanges,
1716
setThumbnail,
@@ -27,7 +26,7 @@ import { setIndex, setIsDisplayEditView } from "../redux/thumbnailSlice";
2726
*/
2827
const ThumbnailSelect: React.FC = () => {
2928

30-
const videoTracks = useAppSelector(selectVideos);
29+
const tracks = useAppSelector(selectTracks);
3130

3231
const thumbnailSelectStyle = css({
3332
display: "flex",
@@ -42,7 +41,7 @@ const ThumbnailSelect: React.FC = () => {
4241

4342
return (
4443
<div css={thumbnailSelectStyle}>
45-
{videoTracks.map((track: Track, index: number) => (
44+
{tracks.map((track: Track, index: number) => (
4645
<ThumbnailSelector
4746
key={index}
4847
track={track}
@@ -177,10 +176,12 @@ const ThumbnailButtons: React.FC<{
177176
track={track}
178177
index={0}
179178
/>
180-
<ToGenerationButton
181-
trackIndex={trackIndex}
182-
index={1}
183-
/>
179+
{track.video_stream.available &&
180+
<ToGenerationButton
181+
trackIndex={trackIndex}
182+
index={1}
183+
/>
184+
}
184185
<DiscardButton
185186
track={track}
186187
index={2}

src/main/Timeline.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useAppDispatch, useAppSelector } from "../redux/store";
99
import { Segment, httpRequestState } from "../types";
1010
import {
1111
selectDuration,
12-
selectVideoURL,
12+
selectTrackURLs,
1313
selectWaveformImages,
1414
setWaveformImages,
1515
selectTimelineZoom,
@@ -687,7 +687,7 @@ export const Waveforms: React.FC<{ timelineHeight: number; topOffset?: number }>
687687
const { t } = useTranslation();
688688

689689
const dispatch = useAppDispatch();
690-
const videoURLs = useAppSelector(selectVideoURL);
690+
const videoURLs = useAppSelector(selectTrackURLs);
691691
const videoURLStatus = useAppSelector((state: { videoState: { status: httpRequestState["status"]; }; }) =>
692692
state.videoState.status);
693693
const theme = useTheme();

src/main/TrackSelection.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import ReactPlayer from "react-player";
66

77
import { Track } from "../types";
88
import {
9+
selectAudiosOnly,
910
selectCustomizedTrackSelection,
11+
selectTracks,
1012
selectVideos,
1113
selectWaveformImages,
1214
setAudioEnabled,
@@ -37,27 +39,35 @@ const TrackSelection: React.FC = () => {
3739
const dispatch = useAppDispatch();
3840

3941
// Generate list of tracks
40-
const tracks = useAppSelector(selectVideos);
42+
const tracks = useAppSelector(selectTracks);
43+
const videos = useAppSelector(selectVideos);
44+
const audioOnlys = useAppSelector(selectAudiosOnly);
45+
4146
let enabledCount = 0;
4247
if (settings.trackSelection.atLeastOneVideo) {
4348
// Only care about at least one video stream being enabled
44-
enabledCount = tracks.reduce(
49+
enabledCount = videos.reduce(
4550
(memo: number, track: Track) =>
4651
memo + (track.video_stream.enabled ? 1 : 0),
4752
0,
4853
);
4954
} else {
5055
// Make sure that at least one track remains enabled
51-
enabledCount = tracks.reduce(
56+
enabledCount += videos.reduce(
5257
(memo: number, track: Track) =>
5358
memo + (track.video_stream.enabled ? 1 : 0) + (track.audio_stream.enabled ? 1 : 0),
5459
0,
5560
);
61+
enabledCount += audioOnlys.reduce(
62+
(memo: number, track: Track) =>
63+
memo + (track.audio_stream.enabled ? 1 : 0),
64+
0,
65+
);
5666
}
5767
const images = useAppSelector(selectWaveformImages);
5868
const customizedTrackSelection = !!useAppSelector(selectCustomizedTrackSelection);
5969

60-
const videoTrackItems = tracks.map(
70+
const videoTrackItems = videos.map(
6171
(track: Track) => (
6272
<VideoTrackItem
6373
key={track.id}
@@ -67,7 +77,7 @@ const TrackSelection: React.FC = () => {
6777
/>),
6878
);
6979

70-
const audioTrackItems = tracks.map(
80+
const audioTrackItems = [...videos, ...audioOnlys].map(
7181
(track: Track, index: number) => (
7282
<AudioTrackItem
7383
key={track.id}

src/main/VideoPlayers.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
setIsPlaying,
1010
selectIsMuted,
1111
selectVolume,
12-
selectVideoCount,
12+
selectTrackCount,
1313
selectDurationInSeconds,
1414
setPreviewTriggered,
1515
selectPreviewTriggered,
@@ -20,7 +20,7 @@ import {
2020
setJumpTriggered,
2121
selectJumpTriggered,
2222
setCurrentlyAt,
23-
selectVideos,
23+
selectTracks,
2424
} from "../redux/videoSlice";
2525

2626
import ReactPlayer, { Config } from "react-player";
@@ -39,6 +39,7 @@ import { useTheme } from "../themes";
3939
import { backgroundBoxStyle } from "../cssStyles";
4040
import { BaseReactPlayerProps } from "react-player/base";
4141
import { ErrorBox } from "@opencast/appkit";
42+
import VideoOffImage from "../img/video-off.png?url";
4243

4344
const VideoPlayers: React.FC<{
4445
refs?: React.MutableRefObject<(VideoPlayerForwardRef | null)[]>,
@@ -50,10 +51,10 @@ const VideoPlayers: React.FC<{
5051
maxHeightInPixel = 300,
5152
}) => {
5253

53-
const videos = useAppSelector(selectVideos);
54+
const videos = useAppSelector(selectTracks);
5455
let primaryIndex = videos.findIndex(e => e.audio_stream.available === true);
5556
primaryIndex = primaryIndex < 0 ? 0 : primaryIndex;
56-
const videoCount = useAppSelector(selectVideoCount);
57+
const videoCount = useAppSelector(selectTrackCount);
5758

5859
const [videoPlayers, setVideoPlayers] = useState<JSX.Element[]>([]);
5960

@@ -81,6 +82,7 @@ const VideoPlayers: React.FC<{
8182
subtitleUrl={""}
8283
first={i === 0}
8384
last={i === videoCount - 1}
85+
audioOnly={!videos[i].video_stream.available}
8486
selectIsPlaying={selectIsPlaying}
8587
selectIsMuted={selectIsMuted}
8688
selectVolume={selectVolume}
@@ -126,6 +128,7 @@ interface VideoPlayerProps {
126128
first: boolean,
127129
last: boolean,
128130
overwritePlayerCSS?: SerializedStyles,
131+
audioOnly?: boolean,
129132
selectIsPlaying: (state: RootState) => boolean,
130133
selectIsMuted: (state: RootState) => boolean,
131134
selectVolume: (state: RootState) => number,
@@ -169,6 +172,7 @@ export const VideoPlayer = React.forwardRef<VideoPlayerForwardRef, VideoPlayerPr
169172
first,
170173
last,
171174
overwritePlayerCSS,
175+
audioOnly = false,
172176
selectCurrentlyAtInSeconds,
173177
selectPreviewTriggered,
174178
selectClickTriggered,
@@ -316,6 +320,7 @@ export const VideoPlayer = React.forwardRef<VideoPlayerForwardRef, VideoPlayerPr
316320
// Skip player when navigating page with keyboard
317321
tabIndex: "-1",
318322
crossOrigin: "anonymous", // allow thumbnail generation
323+
poster: audioOnly && VideoOffImage, // Show image when there is no video stream
319324
},
320325
tracks: [
321326
{ kind: "subtitles", src: subtitleUrl, srcLang: "en", default: true, label: "I am irrelevant" },
@@ -394,6 +399,7 @@ export const VideoPlayer = React.forwardRef<VideoPlayerForwardRef, VideoPlayerPr
394399

395400
const reactPlayerStyle = css({
396401
aspectRatio: "16 / 9", // Hard-coded for now because there are problems with updating this value at runtime
402+
padding: audioOnly ? "0px" : "20px",
397403

398404
overflow: "hidden", // Required for borderRadius to show
399405
...first && {

src/redux/__tests__/videoSlice.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import reducer, { initialState, setIsPlaying, selectIsPlaying, setCurrentlyAt,
22
selectCurrentlyAt, selectActiveSegmentIndex, selectPreviewTriggered,
33
selectDuration, video, cut, selectSegments, markAsDeletedOrAlive, mergeRight,
4-
fetchVideoInformation, selectVideoURL, selectTitle,
5-
selectTracks, selectWorkflows } from "../videoSlice";
4+
fetchVideoInformation, selectTitle,
5+
selectTracks, selectWorkflows, selectTrackURLs } from "../videoSlice";
66
import cloneDeep from "lodash/cloneDeep";
77
import { httpRequestState } from "../../types";
88

@@ -300,12 +300,12 @@ describe("Video reducer", () => {
300300
// Arrange
301301
const resultStatus: httpRequestState = { status: "success", error: undefined, errorReason: "unknown" };
302302
const segments = [{ start: 0, end: 42, deleted: false }];
303-
const videoURLs: video["videoURLs"] = ["video/url"];
303+
const trackURLs: video["trackURLs"] = ["video/url"];
304304
const dur: video["duration"] = 42;
305305
const title: video["title"] = "Video Title";
306306
// const presenters: video["presenters"] = [ "Otto Opencast" ] // Currently missing from the API
307307
const tracks: video["tracks"] = [{
308-
id: "id", uri: videoURLs[0], flavor: { subtype: "prepared", type: "presenter" },
308+
id: "id", uri: trackURLs[0], flavor: { subtype: "prepared", type: "presenter" },
309309
/* eslint-disable camelcase */
310310
video_stream: { available: true, enabled: true, thumbnail_uri: "thumb/url" },
311311
audio_stream: { available: true, enabled: true, thumbnail_uri: "thumb/url" },
@@ -333,7 +333,7 @@ describe("Video reducer", () => {
333333
expect(rootState.videoState).toMatchObject(resultStatus);
334334

335335
expect(selectSegments(rootState)).toMatchObject(segments);
336-
expect(selectVideoURL(rootState)).toMatchObject(videoURLs);
336+
expect(selectTrackURLs(rootState)).toMatchObject(trackURLs);
337337
expect(selectDuration(rootState)).toEqual(dur);
338338
expect(selectTitle(rootState)).toEqual(title);
339339
expect(selectTracks(rootState)).toMatchObject(tracks);

src/redux/videoSlice.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ export interface video {
2929
waveformImages: string[];
3030
originalThumbnails: { id: Track["id"], uri: Track["thumbnailUri"]; }[];
3131

32-
videoURLs: string[], // Links to each video
33-
videoCount: number, // Total number of videos
32+
trackURLs: string[], // Links to each track
33+
trackCount: number, // Total number of tracks
3434
duration: number, // Video duration in milliseconds. Can be null due to Opencast internal error
3535
title: string,
3636
presenters: string[],
@@ -69,8 +69,8 @@ export const initialState: video & httpRequestState = {
6969
waveformImages: [],
7070
originalThumbnails: [],
7171

72-
videoURLs: [],
73-
videoCount: 0,
72+
trackURLs: [],
73+
trackCount: 0,
7474
duration: 0,
7575
title: "",
7676
presenters: [],
@@ -358,9 +358,8 @@ const videoSlice = createSlice({
358358
}
359359
return track;
360360
});
361-
const videos = state.tracks.filter((track: Track) => track.video_stream.available === true);
362-
state.videoURLs = videos.reduce((a: string[], o: { uri: string; }) => (a.push(o.uri), a), []);
363-
state.videoCount = state.videoURLs.length;
361+
state.trackURLs = state.tracks.reduce((a: string[], o: { uri: string; }) => (a.push(o.uri), a), []);
362+
state.trackCount = state.trackURLs.length;
364363
state.subtitlesFromOpencast = payload.subtitles ?
365364
state.subtitlesFromOpencast = payload.subtitles : [];
366365
state.chaptersFromOpencast = payload.chapters ?
@@ -413,8 +412,10 @@ const videoSlice = createSlice({
413412
selectOriginalThumbnails: state => state.originalThumbnails,
414413
// Selectors mainly pertaining to the information fetched from Opencast
415414
selectVideos: state => state.tracks.filter((track: Track) => track.video_stream.available === true),
416-
selectVideoURL: state => state.videoURLs,
417-
selectVideoCount: state => state.videoCount,
415+
selectTrackURLs: state => state.trackURLs,
416+
selectTrackCount: state => state.trackCount,
417+
selectAudiosOnly: state => state.tracks.filter((track: Track) =>
418+
!track.video_stream.available === true && track.audio_stream.available),
418419
selectDuration: state => state.duration,
419420
selectDurationInSeconds: state => state.duration / 1000,
420421
selectTitle: state => state.title,
@@ -628,8 +629,8 @@ export const {
628629
selectTimelineZoom,
629630
selectWaveformImages,
630631
selectOriginalThumbnails,
631-
selectVideoURL,
632-
selectVideoCount,
632+
selectTrackURLs,
633+
selectTrackCount,
633634
selectDuration,
634635
selectDurationInSeconds,
635636
selectTitle,
@@ -641,6 +642,7 @@ export const {
641642
selectChaptersFromOpencast,
642643
selectChaptersFromOpencastById,
643644
selectVideos,
645+
selectAudiosOnly,
644646
selectDisplayDuration,
645647
selectPrimaryThumbnailTrack,
646648
} = videoSlice.selectors;

0 commit comments

Comments
 (0)