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

✨ Allow user to disable video preview #406

Open
wants to merge 3 commits into
base: main
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
4 changes: 2 additions & 2 deletions frontend/src/api/document.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fetcher, makeSwrHook } from '../api';
import { RequestDataType, fetcher, makeSwrHook } from '../api';

export const listDocuments = fetcher.path('/api/v1/documents/').method('get').create();
export const createDocument = fetcher
Expand All @@ -19,7 +19,7 @@ export const useListDocuments = makeSwrHook('listDocuments', listDocuments);
export const useGetDocument = makeSwrHook('getDocument', getDocument);
export const useGetDocumentTasks = makeSwrHook('getDocumentTasks', getDocumentTasks);

export type ApiDocument = ReturnType<typeof useGetDocument>['data'];
export type ApiDocument = RequestDataType<typeof useGetDocument>;

export const deleteDocument = fetcher
.path('/api/v1/documents/{document_id}/')
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/editor/export/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type ExportProps = {
outputNameBase: string;
editor: Editor;
onClose: () => void;
document: ApiDocument;
document?: ApiDocument;
};

export type ExportType = {
Expand Down Expand Up @@ -48,7 +48,7 @@ export function ExportModal({
}: {
onClose: () => void;
editor: Editor;
document: ApiDocument;
document?: ApiDocument;
} & Omit<ComponentProps<typeof Modal>, 'label'>) {
const [exportType, setExportType] = useState(exportTypes[0]);
const ExportBodyComponent = exportType.component;
Expand Down
16 changes: 6 additions & 10 deletions frontend/src/editor/export/transcribee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,19 @@ import { Checkbox } from '../../components/form';
import { downloadBinaryAsFile } from '../../utils/download_text_as_file';
import { ExportProps } from '.';
import { HttpReader, Uint8ArrayReader, ZipWriter, Uint8ArrayWriter } from '@zip.js/zip.js';
import { sortMediaFiles } from '../../utils/use_audio';
import { LoadingSpinnerButton, SecondaryButton } from '../../components/button';
import { splitAndSortMediaFiles } from '../player';

export function TranscribeeExportBody({ onClose, outputNameBase, editor, document }: ExportProps) {
const [loading, setLoading] = useState(false);
const [includeOriginalMediaFile, setIncludeOriginalMediaFile] = useState(false);

const bestMediaUrl = useMemo(() => {
const mappedFiles =
document?.media_files.map((media) => {
return {
src: media.url,
type: media.content_type,
};
}) || [];

return sortMediaFiles(mappedFiles)[0].src;
const { videoSources, audioSources, hasVideo } = splitAndSortMediaFiles(
document?.media_files || [],
);
const bestSource = hasVideo ? videoSources[0] : audioSources[0];
return bestSource.src;
}, [document?.media_files]);

const originalMediaUrl = useMemo(() => {
Expand Down
67 changes: 42 additions & 25 deletions frontend/src/editor/player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import clsx from 'clsx';
import { IconButton } from '../components/button';
import { ImBackward2, ImPause2, ImPlay3 } from 'react-icons/im';
import { useCallback, useMemo, useEffect, useState, useRef, useContext } from 'react';
import { useGetDocument } from '../api/document';
import { ApiDocument, useGetDocument } from '../api/document';
import { CssRule } from '../utils/cssdom';
import { SEEK_TO_EVENT, SeekToEvent } from './types';
import { useEvent } from '../utils/use_event';
Expand All @@ -16,13 +16,36 @@ import { sortMediaFiles, useAudio } from '../utils/use_audio';
import { minutesInMs } from '../utils/duration_in_ms';
import { formattedTime } from './transcription_editor';
import { IconType } from 'react-icons';
import { BiVideo, BiVideoOff } from 'react-icons/bi';

const DOUBLE_TAP_THRESHOLD_MS = 250;
const SKIP_BUTTON_SEC = 2;
const SKIP_SHORTCUT_SEC = 3;

let lastTabPressTs = 0;

export function splitAndSortMediaFiles(mediaFiles: ApiDocument['media_files']) {
const videoFiles = mediaFiles.filter((media) => media.tags.includes('video'));
const audioFiles = mediaFiles.filter((media) => !media.tags.includes('video'));

const mapFile = (media: (typeof mediaFiles)[0]) => {
return {
src: media.url,
type: media.content_type,
tags: media.tags,
};
};

const mappedVideoFiles = videoFiles.map(mapFile);
const mappedAudioFiles = audioFiles.map(mapFile);

return {
videoSources: sortMediaFiles(mappedVideoFiles),
audioSources: sortMediaFiles(mappedAudioFiles),
hasVideo: videoFiles.length > 0,
};
}

export function PlayerBar({
documentId,
editor,
Expand All @@ -40,40 +63,27 @@ export function PlayerBar({
},
);

const [playbackRate, setPlaybackRate] = useLocalStorage('playbackRate', 1);

const { sources, hasVideo } = useMemo(() => {
// do not play the original file, it may be large
const relevantMediaFiles =
data?.media_files.filter((media) => !media.tags.includes('original')) || [];
const { videoSources, audioSources, hasVideo } = useMemo(
() => splitAndSortMediaFiles(data?.media_files || []),
[data?.media_files],
);

const videoFiles = relevantMediaFiles.filter((media) => media.tags.includes('video'));
const audioFiles = relevantMediaFiles.filter((media) => !media.tags.includes('video'));
const [playbackRate, setPlaybackRate] = useLocalStorage('playbackRate', 1);

const mappedFiles = [...videoFiles, ...audioFiles].map((media) => {
return {
src: media.url,
type: media.content_type,
};
});

return {
sources: sortMediaFiles(mappedFiles),
hasVideo: videoFiles.length > 0,
};
}, [data?.media_files]);
const [_showVideo, setShowVideo] = useState(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's please name the state variable consistent with the setter :D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevertheless approved already :)

const showVideo = _showVideo && hasVideo;

const audio = useAudio({
playbackRate,
sources,
videoPreview: hasVideo,
sources: showVideo ? videoSources : audioSources,
videoPreview: showVideo,
});

useEffect(() => {
if (onShowVideo) {
onShowVideo(hasVideo);
onShowVideo(showVideo);
}
}, [hasVideo]);
}, [showVideo]);

// calculate the start of the current element to color it
const [currentElementStartTime, setCurrentElementStartTime] = useState(0.0);
Expand Down Expand Up @@ -207,6 +217,13 @@ export function PlayerBar({
</div>

<PlaybackSpeedDropdown value={playbackRate} onChange={setPlaybackRate} />
{hasVideo && (
<IconButton
icon={showVideo ? BiVideoOff : BiVideo}
label={showVideo ? 'disable video preview' : 'enable video preview'}
onClick={() => setShowVideo(!showVideo)}
/>
)}
</div>
</>
);
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/editor/transcription_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,9 @@ export function TranscriptionEditor({
className={clsx('2xl:-ml-20')}
/>
</ErrorBoundary>
<PlayerBar documentId={documentId} editor={editor} onShowVideo={onShowVideo} />
{!loadingState[0] && (
<PlayerBar documentId={documentId} editor={editor} onShowVideo={onShowVideo} />
)}
</LoadingContext.Provider>
</SpeakerColorsProvider>
</Slate>
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/utils/use_audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,21 @@ const MEDIA_PRIORITY = [
'audio/mpeg',
];

export function sortMediaFiles<T extends { type: string }>(mediaFiles: T[]) {
export function sortMediaFiles<T extends { type: string; tags: string[] }>(mediaFiles: T[]) {
const sorted = [];

const relevantMediaFiles = mediaFiles.filter((media) => !media.tags.includes('original'));
const originalMediaFiles = mediaFiles.filter((media) => media.tags.includes('original'));

for (const contentType of MEDIA_PRIORITY) {
const files = mediaFiles.filter((file) => file.type == contentType);
const files = relevantMediaFiles.filter((file) => file.type == contentType);
sorted.push(...files);
}

const rest = mediaFiles.filter((file) => !MEDIA_PRIORITY.includes(file.type));
const rest = relevantMediaFiles.filter((file) => !MEDIA_PRIORITY.includes(file.type));
sorted.push(...rest);

sorted.push(...originalMediaFiles);

return sorted;
}