Skip to content

Commit

Permalink
Split track selection to video and audio selection
Browse files Browse the repository at this point in the history
In the "Tracks" view, instead of de-/selecting the whole track,
the video and audio streams can be de-/selected individually.
At least one video stream must remain selected. All audio streams
can be deselected. If the audio stream is marked as unavailable,
it cannot be de-/selected.

Shows the waveform from the timeline for a graphical representation
of the audio stream. However, there will only be one waveform
generated for the timeline, so in case of additional tracks a
placeholder image will be shown instead.

Resolves #1009. #1009 also mentions potential issues in the backend,
while this PR only addresses the frontend. From my limited testing
the backend seems fine when using the default community workflows
(no errors, the correct streams get published). If there are any users
around that are still using the track selection feature in the old
editor, and that are aware of any backend issues, please do tell me
about them.
  • Loading branch information
Arnei committed Dec 14, 2023
1 parent 452c507 commit b6c0b9e
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 50 deletions.
Binary file added public/placeholder-waveform-empty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/placeholder-waveform.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 12 additions & 6 deletions src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,18 @@
"trackSelection": {
"title": "Select track(s) for processing",
"trackInactive": "inactive",
"deleteTrackText": "Delete Track",
"restoreTrackText": "Restore Track",
"cannotDeleteTrackText": "Cannot Delete Track",
"deleteTrackTooltip": "Do not encode and publish this track.",
"restoreTrackTooltip": "Encode and publish this track.",
"cannotDeleteTrackTooltip": "Cannot remove this track from publication."
"deleteVideoText": "Delete Video",
"restoreVideoText": "Restore Video",
"cannotDeleteVideoText": "Cannot Delete Video",
"deleteVideoTooltip": "Do not encode and publish this video.",
"restoreVideoTooltip": "Encode and publish this video.",
"cannotDeleteVideoTooltip": "Cannot remove this video from publication.",
"deleteAudioText": "Delete Audio",
"noAudioText": "No Audio available",
"restoreAudioText": "Restore Audio",
"deleteAudioTooltip": "Do not encode and publish this audio.",
"noAudioTooltip": "This track does not have any audio.",
"restoreAudioTooltip": "Encode and publish this audio."
},

"subtitles": {
Expand Down
148 changes: 111 additions & 37 deletions src/main/TrackSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ import React from "react";
import { css } from '@emotion/react'

import { IconType } from "react-icons";
import { LuTrash } from "react-icons/lu";
import { LuTrash, LuXCircle } from "react-icons/lu";
import { ReactComponent as TrashRestore } from '../img/trash-restore.svg';
import ReactPlayer from 'react-player'

import { Track } from '../types'
import { useSelector, useDispatch } from 'react-redux';
import { selectVideos, setTrackEnabled } from '../redux/videoSlice'
import { selectVideos, selectWaveformImages, setAudioEnabled, setVideoEnabled } from '../redux/videoSlice'
import { backgroundBoxStyle, basicButtonStyle, customIconStyle, deactivatedButtonStyle, flexGapReplacementStyle, titleStyle, titleStyleBold } from '../cssStyles'

import { useTranslation } from 'react-i18next';
import { useTheme } from "../themes";
import { ThemedTooltip } from "./Tooltip";
import { outOfBounds } from "../util/utilityFunctions";

/**
* Creates the track selection.
Expand All @@ -23,8 +24,14 @@ const TrackSelection: React.FC = () => {
// Generate list of tracks
const tracks: Track[] = useSelector(selectVideos);
const enabledCount = tracks.filter(t => t.video_stream.enabled).length;
const trackItems: JSX.Element[] = tracks.map((track: Track) =>
<TrackItem key={track.id} track={track} enabledCount={enabledCount} />
const images = useSelector(selectWaveformImages)
const trackItems: JSX.Element[] = tracks.map((track: Track, index: number) =>
<TrackItem
key={track.id}
track={track}
enabledCount={enabledCount}
waveform={outOfBounds(images, index) ? undefined : images[index]}
/>
);

const trackSelectionStyle = css({
Expand Down Expand Up @@ -71,15 +78,15 @@ const Header: React.FC = () => {
}


const TrackItem: React.FC<{track: Track, enabledCount: number}> = ({track, enabledCount}) => {
const TrackItem: React.FC<{track: Track, enabledCount: number, waveform: string | undefined}> = ({track, enabledCount, waveform}) => {

const theme = useTheme()

const { t } = useTranslation();
const dispatch = useDispatch();
const header = track.flavor.type + ' '
+ (track.video_stream.enabled ? ''
: `(${t('trackSelection.trackInactive', 'inactive')})`);
const header = track.flavor.type

const imagesMaxWidth = 475

const trackItemStyle = css({
display: 'flex',
Expand All @@ -96,12 +103,27 @@ const TrackItem: React.FC<{track: Track, enabledCount: number}> = ({track, enabl
flexWrap: 'wrap',
})

const imagesStyle = css({
display: 'flex',
flexDirection: 'column',
...(flexGapReplacementStyle(20, true)),
})

const playerStyle = css({
aspectRatio: '16 / 9',
width: '100%',
maxWidth: '457px',
maxWidth: `${imagesMaxWidth}px`,
});

const imgStyle = css({
height: '54px', // Keep height consistent in case the image does not render
width: '100%',
maxWidth: `${imagesMaxWidth}px`,

filter: `${theme.invert_wave}`,
color: `${theme.inverted_text}`,
})

const headerStyle = css({
fontWeight: 'bold',
fontSize: 'larger',
Expand All @@ -120,49 +142,99 @@ const TrackItem: React.FC<{track: Track, enabledCount: number}> = ({track, enabl
flexDirection: 'column',
})

// What state is the track in and can it be deactivated?
// We do not permit deactivating the last remaining track
// 2 -> Track is enabled and can be deactivated
// 1 -> Track is enabled but is the last and cannot be deactivated
// 0 -> Track is disabled and can be restored
// What state is the video stream in and can it be deactivated?
// We do not permit deactivating the last remaining video
// 2 -> Video is enabled and can be deactivated
// 1 -> Video is enabled but is the last and cannot be deactivated
// 0 -> Video is disabled and can be restored
const deleteStatus = track.video_stream.enabled ? (enabledCount > 1 ? 0 : 1) : 2;
const deleteEnabled = deleteStatus !== 1;
const deleteText = [
t('trackSelection.deleteTrackText', 'Delete Track'),
t('trackSelection.cannotDeleteTrackText', 'Cannot Delete Track'),
t('trackSelection.restoreTrackText', 'Restore Track')
const deleteTextVideo = [
t('trackSelection.deleteVideoText'),
t('trackSelection.cannotDeleteVideoText'),
t('trackSelection.restoreVideoText')
][deleteStatus];
const deleteTooltip = [
t('trackSelection.deleteTrackTooltip', 'Do not encode and publish this track.'),
t('trackSelection.cannotDeleteTrackTooltip', 'Cannot remove this track from publication.'),
t('trackSelection.restoreTrackTooltip', 'Encode and publish this track.')
const deleteTooltipVideo = [
t('trackSelection.deleteVideoTooltip'),
t('trackSelection.cannotDeleteVideoTooltip'),
t('trackSelection.restoreVideoTooltip')
][deleteStatus];
const deleteIcon = [LuTrash, LuTrash, TrashRestore][deleteStatus];
const trackEnabledChange = () => {
dispatch(setTrackEnabled({
id: track.id,
const deleteIcon = [LuTrash, LuXCircle, TrashRestore][deleteStatus];
const videoEnabledChange = () => {
dispatch(setVideoEnabled({
trackId: track.id,
enabled: !track.video_stream.enabled,
}))
}

// What state is the audio stream in and can it be deactivated?
// 2 -> Audio is enabled and can be deactivated
// 1 -> Audio is not available on this track and thus cannot be de-/activated
// 0 -> Audio is disabled and can be restored
const deleteStatusAudio = track.audio_stream.available ? (track.audio_stream.enabled ? 0 : 2) : 1;
const deleteEnabledAudio = deleteStatusAudio !== 1;
const deleteTextAudio = [
t('trackSelection.deleteAudioText'),
t('trackSelection.noAudioText'),
t('trackSelection.restoreAudioText')
][deleteStatusAudio];
const deleteTooltipAudio = [
t('trackSelection.deleteAudioTooltip'),
t('trackSelection.noAudioTooltip'),
t('trackSelection.restoreAudioTooltip')
][deleteStatusAudio];
const deleteIconAudio = [LuTrash, LuXCircle, TrashRestore][deleteStatusAudio];
const audioEnabledChange = () => {
dispatch(setAudioEnabled({
trackId: track.id,
enabled: !track.audio_stream.enabled,
}))
}

return (
<div css={[backgroundBoxStyle(theme), trackItemStyle]}>
<div css={headerStyle}>{ header }</div>
<div css={trackitemSubStyle}>
<ReactPlayer
width="unset"
height="unset"
css={playerStyle}
style={{opacity: track.video_stream.enabled ? '1' : '0.5'}}
url={track.uri}
/>
<div css={imagesStyle}>
<ReactPlayer
width="unset"
height="unset"
css={playerStyle}
style={{opacity: track.video_stream.enabled ? '1' : '0.5'}}
url={track.uri}
/>
{track.audio_stream.available ?
<img
src={waveform ?? "/placeholder-waveform.png"}
css={imgStyle}
style={{opacity: track.audio_stream.enabled ? '1' : '0.5'}}
alt="placeholder for audio stream"
/>
:
<img
src="/placeholder-waveform-empty.png"
css={imgStyle}
style={{opacity: '0.5'}}
alt="placeholder for unavailable audio stream"
/>
}
</div>
<div css={buttonsStyle}>
<SelectButton
text={deleteText}
tooltip={deleteTooltip}
handler={trackEnabledChange}
text={deleteTextVideo}
tooltip={deleteTooltipVideo}
handler={videoEnabledChange}
Icon={deleteIcon}
active={deleteEnabled}
positionAtEnd={false}
/>
<SelectButton
text={deleteTextAudio}
tooltip={deleteTooltipAudio}
handler={audioEnabledChange}
Icon={deleteIconAudio}
active={deleteEnabledAudio}
positionAtEnd={true}
/>
</div>
</div>
Expand All @@ -176,9 +248,10 @@ interface selectButtonInterface {
Icon: IconType | React.FunctionComponent,
tooltip: string,
active: boolean,
positionAtEnd: boolean, // Just here to align the audio button with the corresponding image
}

const SelectButton : React.FC<selectButtonInterface> = ({handler, text, Icon, tooltip, active}) => {
const SelectButton : React.FC<selectButtonInterface> = ({handler, text, Icon, tooltip, active, positionAtEnd}) => {

const theme = useTheme();

Expand All @@ -190,6 +263,7 @@ const SelectButton : React.FC<selectButtonInterface> = ({handler, text, Icon, to
boxShadow: '',
background: `${theme.element_bg}`,
textWrap: 'nowrap',
...(positionAtEnd && {marginTop: 'auto'}),
}];

const clickHandler = () => {
Expand Down
21 changes: 14 additions & 7 deletions src/redux/videoSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,22 @@ const videoSlice = createSlice({
name: 'videoState',
initialState,
reducers: {
setTrackEnabled: (state, action) => {
setVideoEnabled: (state, action: PayloadAction<{trackId: string, enabled: boolean}>) => {
for (const track of state.tracks) {
if (track.id === action.payload.id) {
track.audio_stream.enabled = action.payload.enabled;
if (track.id === action.payload.trackId) {
track.video_stream.enabled = action.payload.enabled;
}
}
state.hasChanges = true;
},
setAudioEnabled: (state, action: PayloadAction<{trackId: string, enabled: boolean}>) => {
for (const track of state.tracks) {
if (track.id === action.payload.trackId) {
track.audio_stream.enabled = action.payload.enabled;
}
}
state.hasChanges = true;
},
setIsPlaying: (state, action: PayloadAction<video["isPlaying"]>) => {
state.isPlaying = action.payload;
},
Expand Down Expand Up @@ -378,10 +385,10 @@ const setThumbnailHelper = (state: video, id: Track["id"], uri: Track["thumbnail
}
}

export const { setTrackEnabled, setIsPlaying, setIsPlayPreview, setIsMuted, setVolume, setCurrentlyAt, setCurrentlyAtInSeconds,
addSegment, setAspectRatio, setHasChanges, setWaveformImages, setThumbnails, setThumbnail, removeThumbnail,
setLock, cut, markAsDeletedOrAlive, setSelectedWorkflowIndex, mergeLeft, mergeRight, mergeAll, setPreviewTriggered,
setClickTriggered } = videoSlice.actions
export const { setVideoEnabled, setAudioEnabled, setIsPlaying, setIsPlayPreview, setIsMuted, setVolume, setCurrentlyAt,
setCurrentlyAtInSeconds, addSegment, setAspectRatio, setHasChanges, setWaveformImages, setThumbnails, setThumbnail,
removeThumbnail, setLock, cut, markAsDeletedOrAlive, setSelectedWorkflowIndex, mergeLeft, mergeRight, mergeAll,
setPreviewTriggered, setClickTriggered } = videoSlice.actions

// Export selectors
// Selectors mainly pertaining to the video state
Expand Down
8 changes: 8 additions & 0 deletions src/util/utilityFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,11 @@ export function useInterval(callback: IntervalFunction, delay: number | null) {
}
}, [callback, delay]);
}

// Returns true if the given index is out of bounds on the given array
export function outOfBounds(array: any[], index: number) {
if (index >= array.length) {
return true
}
return false
}

0 comments on commit b6c0b9e

Please sign in to comment.