Skip to content

Commit

Permalink
Better UX when attempting to record DRM-protected stream on firefox
Browse files Browse the repository at this point in the history
- Creates flashcard anyway, but shows error message in audio field,
  instead of blocking mining altogether with an error dialog.
  • Loading branch information
killergerbah committed Apr 20, 2024
1 parent df3ae66 commit 55a4053
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 72 deletions.
5 changes: 3 additions & 2 deletions common/anki/anki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ export async function exportCard(card: CardModel, ankiSettings: AnkiSettings, ex
card.subtitle.end,
card.audio.playbackRate ?? 1,
card.audio.base64,
card.audio.extension
card.audio.extension,
card.audio.error
);

return await anki.export(
Expand Down Expand Up @@ -240,7 +241,7 @@ export class Anki {
const gui = mode === 'gui';
const updateLast = mode === 'updateLast';

if (this.settingsProvider.audioField && audioClip && audioClip.isPlayable()) {
if (this.settingsProvider.audioField && audioClip && audioClip.error !== undefined) {
const sanitizedName = this._sanitizeFileName(audioClip.name);
const data = await audioClip.base64();

Expand Down
4 changes: 2 additions & 2 deletions common/app/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -565,14 +565,14 @@ function App({ origin, logoUrl, settings, extension, fetcher, onSettingsChanged
try {
const clip = AudioClip.fromCard(card, settings.audioPaddingStart, settings.audioPaddingEnd);

if (clip?.isPlayable()) {
if (clip?.error === undefined) {
if (settings.preferMp3) {
clip!.toMp3(mp3WorkerFactory).download();
} else {
clip!.download();
}
} else {
handleError(t('ankiDialog.audioFileLinkLost'));
handleError(t(clip.errorLocKey!));
}
} catch (e) {
handleError(e);
Expand Down
2 changes: 1 addition & 1 deletion common/app/components/CopyHistoryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const useAudioAvailability = (item: CopyHistoryItem) => {
const clip = AudioClip.fromCard(item, 0, 0);

if (clip) {
setIsAudioAvailable(clip.isPlayable());
setIsAudioAvailable(clip.error === undefined);
} else {
setIsAudioAvailable(false);
}
Expand Down
56 changes: 41 additions & 15 deletions common/audio-clip/audio-clip.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Mp3Encoder from './mp3-encoder';

import { CardModel, FileModel } from '@project/common';
import { AudioErrorCode, CardModel, FileModel } from '@project/common';
import { download } from '@project/common/util';
import { isActiveBlobUrl } from '../blob-url';

Expand All @@ -27,7 +27,7 @@ interface AudioData {
base64: () => Promise<string>;
slice: (start: number, end: number) => AudioData;
isSliceable: () => boolean;
isPlayable: () => boolean;
error?: AudioErrorCode;
}

function recorderConfiguration() {
Expand All @@ -47,18 +47,28 @@ class Base64AudioData implements AudioData {
private readonly playbackRate: number;
private readonly _base64: string;
private readonly _extension: string;
private readonly _error?: AudioErrorCode;

private playingAudio?: HTMLAudioElement;
private stopAudioTimeout?: NodeJS.Timeout;
private cachedBlob?: Blob;

constructor(baseName: string, start: number, end: number, playbackRate: number, base64: string, extension: string) {
constructor(
baseName: string,
start: number,
end: number,
playbackRate: number,
base64: string,
extension: string,
error: AudioErrorCode | undefined
) {
this._name = makeFileName(baseName, start);
this._start = start;
this._end = end;
this.playbackRate = playbackRate;
this._base64 = base64;
this._extension = extension;
this._error = error;
}

get name(): string {
Expand Down Expand Up @@ -142,8 +152,8 @@ class Base64AudioData implements AudioData {
return false;
}

isPlayable() {
return true;
get error() {
return this._error;
}
}

Expand Down Expand Up @@ -404,12 +414,12 @@ class FileAudioData implements AudioData {
return true;
}

isPlayable() {
get error() {
if (this.file.blobUrl) {
return isActiveBlobUrl(this.file.blobUrl);
return isActiveBlobUrl(this.file.blobUrl) ? undefined : AudioErrorCode.fileLinkLost;
}

return false;
return undefined;
}
}

Expand Down Expand Up @@ -479,8 +489,8 @@ class Mp3AudioData implements AudioData {
return this.data.isSliceable();
}

isPlayable() {
return this.data.isPlayable();
get error() {
return this.data.error;
}
}

Expand All @@ -502,7 +512,8 @@ export default class AudioClip {
end + (card.audio.paddingEnd ?? 0),
card.audio.playbackRate ?? 1,
card.audio.base64,
card.audio.extension
card.audio.extension,
card.audio.error
);
}

Expand All @@ -525,7 +536,8 @@ export default class AudioClip {
end: number,
playbackRate: number,
base64: string,
extension: string
extension: string,
error: AudioErrorCode | undefined
) {
return new AudioClip(
new Base64AudioData(
Expand All @@ -534,7 +546,8 @@ export default class AudioClip {
end,
playbackRate,
base64,
extension
extension,
error
)
);
}
Expand Down Expand Up @@ -596,7 +609,20 @@ export default class AudioClip {
return this.data.isSliceable();
}

isPlayable() {
return this.data.isPlayable();
get error() {
return this.data.error;
}

get errorLocKey() {
if (this.data.error === undefined) {
return undefined;
}

switch (this.data.error) {
case AudioErrorCode.drmProtected:
return 'audioCaptureFailed.message';
case AudioErrorCode.fileLinkLost:
return 'ankiDialog.audioFileLinkLost';
}
}
}
14 changes: 9 additions & 5 deletions common/components/AnkiDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { MutableRefObject, useCallback, useState, useEffect, useMemo } fr
import { useTranslation } from 'react-i18next';
import makeStyles from '@material-ui/core/styles/makeStyles';
import withStyles from '@material-ui/core/styles/withStyles';
import { Image, SubtitleModel, CardModel } from '@project/common';
import { Image, SubtitleModel, CardModel, AudioErrorCode } from '@project/common';
import { AnkiSettings } from '@project/common/settings';
import {
humanReadableTime,
Expand Down Expand Up @@ -173,7 +173,7 @@ const useAudioHelperText = (audioClip?: AudioClip, onRerecord?: () => void) => {

useEffect(() => {
if (audioClip) {
const playable = audioClip.isPlayable();
const playable = audioClip.error === undefined;
setAudioClipPlayable(playable);

if (playable) {
Expand All @@ -183,7 +183,7 @@ const useAudioHelperText = (audioClip?: AudioClip, onRerecord?: () => void) => {
setAudioHelperText(undefined);
}
} else {
setAudioHelperText(t('ankiDialog.audioFileLinkLost')!);
setAudioHelperText(t(audioClip.errorLocKey!)!);
}
}
}, [audioClip, onRerecord, t]);
Expand Down Expand Up @@ -469,7 +469,7 @@ const AnkiDialog = ({

const handlePlayAudio = useCallback(
async (e: React.MouseEvent<HTMLDivElement>) => {
if (!audioClip?.isPlayable()) {
if (audioClip?.error !== undefined) {
return;
}

Expand Down Expand Up @@ -649,7 +649,11 @@ const AnkiDialog = ({
audioActionElement = (
<Tooltip title={t('ankiDialog.rerecord')!}>
<span>
<IconButton onClick={handleApplyTimestampIntervalToAudio} edge="end">
<IconButton
disabled={audioClip?.error !== undefined}
onClick={handleApplyTimestampIntervalToAudio}
edge="end"
>
<FiberManualRecordIcon />
</IconButton>
</span>
Expand Down
6 changes: 6 additions & 0 deletions common/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export interface ImageModel {
readonly extension: 'jpeg';
}

export enum AudioErrorCode {
drmProtected = 1,
fileLinkLost = 2,
}

export interface AudioModel {
readonly base64: string;
readonly extension: 'webm' | 'mp3';
Expand All @@ -72,6 +77,7 @@ export interface AudioModel {
readonly start?: number;
readonly end?: number;
readonly playbackRate?: number;
readonly error?: AudioErrorCode;
}

export interface AnkiUiState extends CardTextFieldValues {
Expand Down
2 changes: 1 addition & 1 deletion extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const handlers: CommandHandler[] = [
new VideoHeartbeatHandler(tabRegistry),
new RecordMediaHandler(audioRecorder, imageCapturer, cardPublisher, settings),
new RerecordMediaHandler(settings, audioRecorder, cardPublisher),
new StartRecordingMediaHandler(audioRecorder, imageCapturer, cardPublisher),
new StartRecordingMediaHandler(audioRecorder, imageCapturer, cardPublisher, settings),
new StopRecordingMediaHandler(audioRecorder, imageCapturer, cardPublisher, settings),
new TakeScreenshotHandler(imageCapturer, cardPublisher),
new ToggleSubtitlesHandler(settings, tabRegistry),
Expand Down
36 changes: 29 additions & 7 deletions extension/src/handlers/video/record-media-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import {
VideoToExtensionCommand,
ExtensionToVideoCommand,
ScreenshotTakenMessage,
RecordingFinishedMessage,
CardModel,
AudioErrorCode,
} from '@project/common';
import { SettingsProvider } from '@project/common/settings';
import { CardPublisher } from '../../services/card-publisher';
import AudioRecorderService from '../../services/audio-recorder-service';
import { DrmProtectedStreamError } from '../../services/audio-recorder-delegate';

export default class RecordMediaHandler {
private readonly _audioRecorder: AudioRecorderService;
Expand Down Expand Up @@ -90,14 +91,35 @@ export default class RecordMediaHandler {
}

if (audioPromise) {
const audioBase64 = await audioPromise;
audioModel = {
base64: audioBase64,
const {
audioPaddingStart: paddingStart,
audioPaddingEnd: paddingEnd,
playbackRate,
} = recordMediaCommand.message;
const baseAudioModel: AudioModel = {
base64: '',
extension: preferMp3 ? 'mp3' : 'webm',
paddingStart: recordMediaCommand.message.audioPaddingStart,
paddingEnd: recordMediaCommand.message.audioPaddingEnd,
playbackRate: recordMediaCommand.message.playbackRate,
paddingStart,
paddingEnd,
playbackRate,
};

try {
const audioBase64 = await audioPromise;
audioModel = {
...baseAudioModel,
base64: audioBase64,
};
} catch (e) {
if (!(e instanceof DrmProtectedStreamError)) {
throw e;
}

audioModel = {
...baseAudioModel,
error: AudioErrorCode.drmProtected,
};
}
}

if (imagePromise) {
Expand Down
34 changes: 26 additions & 8 deletions extension/src/handlers/video/rerecord-media-handler.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import {
AudioErrorCode,
AudioModel,
Command,
ExtensionToVideoCommand,
Message,
RecordingFinishedMessage,
RerecordMediaMessage,
ShowAnkiUiAfterRerecordMessage,
VideoToExtensionCommand,
} from '@project/common';
import { CardPublisher } from '../../services/card-publisher';
import { SettingsProvider } from '@project/common/settings';
import AudioRecorderService from '../../services/audio-recorder-service';
import { DrmProtectedStreamError } from '../../services/audio-recorder-delegate';

export default class RerecordMediaHandler {
private readonly _settingsProvider: SettingsProvider;
Expand All @@ -34,13 +35,8 @@ export default class RerecordMediaHandler {
async handle(command: Command<Message>, sender: chrome.runtime.MessageSender) {
const rerecordCommand = command as VideoToExtensionCommand<RerecordMediaMessage>;
const preferMp3 = await this._settingsProvider.getSingle('preferMp3');
const audio: AudioModel = {
base64: await this._audioRecorder.startWithTimeout(
rerecordCommand.message.duration / rerecordCommand.message.playbackRate +
rerecordCommand.message.audioPaddingEnd,
preferMp3,
{ src: rerecordCommand.src, tabId: sender.tab?.id! }
),
const baseAudioModel: AudioModel = {
base64: '',
extension: preferMp3 ? 'mp3' : 'webm',
paddingStart: rerecordCommand.message.audioPaddingStart,
paddingEnd: rerecordCommand.message.audioPaddingEnd,
Expand All @@ -50,6 +46,28 @@ export default class RerecordMediaHandler {
rerecordCommand.message.duration / rerecordCommand.message.playbackRate,
playbackRate: rerecordCommand.message.playbackRate,
};
let audio: AudioModel;

try {
audio = {
...baseAudioModel,
base64: await this._audioRecorder.startWithTimeout(
rerecordCommand.message.duration / rerecordCommand.message.playbackRate +
rerecordCommand.message.audioPaddingEnd,
preferMp3,
{ src: rerecordCommand.src, tabId: sender.tab?.id! }
),
};
} catch (e) {
if (!(e instanceof DrmProtectedStreamError)) {
throw e;
}

audio = {
...baseAudioModel,
error: AudioErrorCode.drmProtected,
};
}

this._cardPublisher.publish(
{
Expand Down
Loading

0 comments on commit 55a4053

Please sign in to comment.