mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
better subtitle handling
This commit is contained in:
parent
4d5f03337d
commit
307f555b70
10 changed files with 76 additions and 140 deletions
|
@ -19,7 +19,6 @@
|
||||||
"json5": "^2.2.0",
|
"json5": "^2.2.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
"node-webvtt": "^1.9.4",
|
|
||||||
"ofetch": "^1.0.0",
|
"ofetch": "^1.0.0",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
@ -31,7 +30,7 @@
|
||||||
"react-stickynode": "^4.1.0",
|
"react-stickynode": "^4.1.0",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"srt-webvtt": "^2.0.0",
|
"subsrt-ts": "^2.1.0",
|
||||||
"unpacker": "^1.0.1"
|
"unpacker": "^1.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
27
src/@types/node_webtt.d.ts
vendored
27
src/@types/node_webtt.d.ts
vendored
|
@ -1,27 +0,0 @@
|
||||||
declare module "node-webvtt" {
|
|
||||||
interface Cue {
|
|
||||||
identifier: string;
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
text: string;
|
|
||||||
styles: string;
|
|
||||||
}
|
|
||||||
interface Options {
|
|
||||||
meta?: boolean;
|
|
||||||
strict?: boolean;
|
|
||||||
}
|
|
||||||
type ParserError = Error;
|
|
||||||
interface ParseResult {
|
|
||||||
valid: boolean;
|
|
||||||
strict: boolean;
|
|
||||||
cues: Cue[];
|
|
||||||
errors: ParserError[];
|
|
||||||
meta?: Map<string, string>;
|
|
||||||
}
|
|
||||||
interface Segment {
|
|
||||||
duration: number;
|
|
||||||
cues: Cue[];
|
|
||||||
}
|
|
||||||
function parse(text: string, options: Options): ParseResult;
|
|
||||||
function segment(input: string, segmentLength?: number): Segment[];
|
|
||||||
}
|
|
|
@ -1,48 +1,26 @@
|
||||||
import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch";
|
import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch";
|
||||||
import { MWCaption, MWCaptionType } from "@/backend/helpers/streams";
|
import { MWCaption } from "@/backend/helpers/streams";
|
||||||
import toWebVTT from "srt-webvtt";
|
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
|
import { list, parse, detect } from "subsrt-ts";
|
||||||
|
import { ContentCaption } from "subsrt-ts/dist/types/handler";
|
||||||
|
|
||||||
|
export const subtitleTypeList = list()
|
||||||
|
.map((v) => `.${v}`)
|
||||||
|
.join(",");
|
||||||
export const sanitize = DOMPurify.sanitize;
|
export const sanitize = DOMPurify.sanitize;
|
||||||
export const CUSTOM_CAPTION_ID = "customCaption";
|
|
||||||
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
||||||
if (caption.type === MWCaptionType.SRT) {
|
if (caption.url.startsWith("blob:")) return caption.url;
|
||||||
let captionBlob: Blob;
|
let captionBlob: Blob;
|
||||||
|
if (caption.needsProxy) {
|
||||||
if (caption.needsProxy) {
|
captionBlob = await proxiedFetch<Blob>(caption.url, {
|
||||||
captionBlob = await proxiedFetch<Blob>(caption.url, {
|
responseType: "blob" as any,
|
||||||
responseType: "blob" as any,
|
});
|
||||||
});
|
} else {
|
||||||
} else {
|
captionBlob = await mwFetch<Blob>(caption.url, {
|
||||||
captionBlob = await mwFetch<Blob>(caption.url, {
|
responseType: "blob" as any,
|
||||||
responseType: "blob" as any,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return toWebVTT(captionBlob);
|
|
||||||
}
|
}
|
||||||
|
return URL.createObjectURL(captionBlob);
|
||||||
if (caption.type === MWCaptionType.VTT) {
|
|
||||||
if (caption.needsProxy) {
|
|
||||||
const blob = await proxiedFetch<Blob>(caption.url, {
|
|
||||||
responseType: "blob" as any,
|
|
||||||
});
|
|
||||||
return URL.createObjectURL(blob);
|
|
||||||
}
|
|
||||||
|
|
||||||
return caption.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("invalid type");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function convertCustomCaptionFileToWebVTT(file: File) {
|
|
||||||
const header = await file.slice(0, 6).text();
|
|
||||||
const isWebVTT = header === "WEBVTT";
|
|
||||||
if (!isWebVTT) {
|
|
||||||
return toWebVTT(file);
|
|
||||||
}
|
|
||||||
return URL.createObjectURL(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function revokeCaptionBlob(url: string | undefined) {
|
export function revokeCaptionBlob(url: string | undefined) {
|
||||||
|
@ -50,3 +28,12 @@ export function revokeCaptionBlob(url: string | undefined) {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseSubtitles(text: string): ContentCaption[] {
|
||||||
|
if (detect(text) === "") {
|
||||||
|
throw new Error("Invalid subtitle format");
|
||||||
|
}
|
||||||
|
return parse(text)
|
||||||
|
.filter((cue) => cue.type === "caption")
|
||||||
|
.map((cue) => cue as ContentCaption);
|
||||||
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ export enum MWStreamQuality {
|
||||||
export type MWCaption = {
|
export type MWCaption = {
|
||||||
needsProxy?: boolean;
|
needsProxy?: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
type: MWCaptionType;
|
type?: MWCaptionType;
|
||||||
langIso: string;
|
langIso: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@
|
||||||
"noCaptions": "No captions",
|
"noCaptions": "No captions",
|
||||||
"linkedCaptions": "Linked captions",
|
"linkedCaptions": "Linked captions",
|
||||||
"customCaption": "Custom caption",
|
"customCaption": "Custom caption",
|
||||||
"uploadCustomCaption": "Upload caption (SRT, VTT)",
|
"uploadCustomCaption": "Upload caption",
|
||||||
"noEmbeds": "No embeds were found for this source",
|
"noEmbeds": "No embeds were found for this source",
|
||||||
"errors": {
|
"errors": {
|
||||||
"loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}",
|
"loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}",
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
"noCaptions": "Pas de sous-titres",
|
"noCaptions": "Pas de sous-titres",
|
||||||
"linkedCaptions": "Sous-titres liés",
|
"linkedCaptions": "Sous-titres liés",
|
||||||
"customCaption": "Sous-titres personnalisés",
|
"customCaption": "Sous-titres personnalisés",
|
||||||
"uploadCustomCaption": "Télécharger des sous-titres (SRT, VTT)",
|
"uploadCustomCaption": "Télécharger des sous-titres",
|
||||||
"noEmbeds": "Aucun contenu intégré n'a été trouvé pour cette source",
|
"noEmbeds": "Aucun contenu intégré n'a été trouvé pour cette source",
|
||||||
"errors": {
|
"errors": {
|
||||||
"loadingWentWong": "Un problème est survenu lors du chargement des épisodes pour {{seasonTitle}}",
|
"loadingWentWong": "Un problème est survenu lors du chargement des épisodes pour {{seasonTitle}}",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
import { useSettings } from "@/state/settings";
|
import { useSettings } from "@/state/settings";
|
||||||
import { sanitize } from "@/backend/helpers/captions";
|
import { sanitize, parseSubtitles } from "@/backend/helpers/captions";
|
||||||
import { parse, Cue } from "node-webvtt";
|
import { ContentCaption } from "subsrt-ts/dist/types/handler";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
import { useVideoPlayerDescriptor } from "../../state/hooks";
|
import { useVideoPlayerDescriptor } from "../../state/hooks";
|
||||||
|
@ -48,16 +48,18 @@ export function CaptionRendererAction({
|
||||||
const source = useSource(descriptor).source;
|
const source = useSource(descriptor).source;
|
||||||
const videoTime = useProgress(descriptor).time;
|
const videoTime = useProgress(descriptor).time;
|
||||||
const { captionSettings } = useSettings();
|
const { captionSettings } = useSettings();
|
||||||
const captions = useRef<Cue[]>([]);
|
const captions = useRef<ContentCaption[]>([]);
|
||||||
|
|
||||||
useAsync(async () => {
|
useAsync(async () => {
|
||||||
const url = source?.caption?.url;
|
const blobUrl = source?.caption?.url;
|
||||||
if (url) {
|
if (blobUrl) {
|
||||||
// Is there a better way?
|
const result = await fetch(blobUrl);
|
||||||
const result = await fetch(url);
|
|
||||||
// Uses UTF-8 by default
|
|
||||||
const text = await result.text();
|
const text = await result.text();
|
||||||
captions.current = parse(text, { strict: false }).cues;
|
try {
|
||||||
|
captions.current = parseSubtitles(text);
|
||||||
|
} catch (error) {
|
||||||
|
captions.current = [];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
captions.current = [];
|
captions.current = [];
|
||||||
}
|
}
|
||||||
|
@ -65,8 +67,8 @@ export function CaptionRendererAction({
|
||||||
|
|
||||||
if (!captions.current.length) return null;
|
if (!captions.current.length) return null;
|
||||||
const isVisible = (start: number, end: number): boolean => {
|
const isVisible = (start: number, end: number): boolean => {
|
||||||
const delayedStart = start + captionSettings.delay;
|
const delayedStart = start / 1000 + captionSettings.delay;
|
||||||
const delayedEnd = end + captionSettings.delay;
|
const delayedEnd = end / 1000 + captionSettings.delay;
|
||||||
return (
|
return (
|
||||||
Math.max(0, delayedStart) <= videoTime &&
|
Math.max(0, delayedStart) <= videoTime &&
|
||||||
Math.max(0, delayedEnd) >= videoTime
|
Math.max(0, delayedEnd) >= videoTime
|
||||||
|
@ -82,9 +84,9 @@ export function CaptionRendererAction({
|
||||||
show
|
show
|
||||||
>
|
>
|
||||||
{captions.current.map(
|
{captions.current.map(
|
||||||
({ identifier, end, start, text }) =>
|
({ start, end, content }) =>
|
||||||
isVisible(start, end) && (
|
isVisible(start, end) && (
|
||||||
<CaptionCue key={identifier || `${start}-${end}`} text={text} />
|
<CaptionCue key={`${start}-${end}`} text={content} />
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {
|
import {
|
||||||
getCaptionUrl,
|
getCaptionUrl,
|
||||||
convertCustomCaptionFileToWebVTT,
|
parseSubtitles,
|
||||||
CUSTOM_CAPTION_ID,
|
subtitleTypeList,
|
||||||
} from "@/backend/helpers/captions";
|
} from "@/backend/helpers/captions";
|
||||||
import { MWCaption } from "@/backend/helpers/streams";
|
import { MWCaption } from "@/backend/helpers/streams";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
@ -13,10 +13,11 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useSource } from "@/video/state/logic/source";
|
import { useSource } from "@/video/state/logic/source";
|
||||||
import { ChangeEvent, useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
||||||
|
|
||||||
|
const customCaption = "external-custom";
|
||||||
function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
|
function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
|
||||||
return isLinked ? `linked-${caption.langIso}` : `external-${caption.langIso}`;
|
return isLinked ? `linked-${caption.langIso}` : `external-${caption.langIso}`;
|
||||||
}
|
}
|
||||||
|
@ -41,35 +42,17 @@ export function CaptionSelectionPopout(props: {
|
||||||
async (caption: MWCaption, isLinked: boolean) => {
|
async (caption: MWCaption, isLinked: boolean) => {
|
||||||
const id = makeCaptionId(caption, isLinked);
|
const id = makeCaptionId(caption, isLinked);
|
||||||
loadingId.current = id;
|
loadingId.current = id;
|
||||||
controls.setCaption(id, await getCaptionUrl(caption));
|
const blobUrl = await getCaptionUrl(caption);
|
||||||
|
const result = await fetch(blobUrl);
|
||||||
|
const text = await result.text();
|
||||||
|
parseSubtitles(text); // This will throw if the file is invalid
|
||||||
|
controls.setCaption(id, blobUrl);
|
||||||
controls.closePopout();
|
controls.closePopout();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentCaption = source.source?.caption?.id;
|
const currentCaption = source.source?.caption?.id;
|
||||||
const customCaptionUploadElement = useRef<HTMLInputElement>(null);
|
const customCaptionUploadElement = useRef<HTMLInputElement>(null);
|
||||||
const [setCustomCaption, loadingCustomCaption, errorCustomCaption] =
|
|
||||||
useLoading(async (captionFile: File) => {
|
|
||||||
if (
|
|
||||||
!captionFile.name.endsWith(".srt") &&
|
|
||||||
!captionFile.name.endsWith(".vtt")
|
|
||||||
) {
|
|
||||||
throw new Error("Only SRT or VTT files are allowed");
|
|
||||||
}
|
|
||||||
controls.setCaption(
|
|
||||||
CUSTOM_CAPTION_ID,
|
|
||||||
await convertCustomCaptionFileToWebVTT(captionFile)
|
|
||||||
);
|
|
||||||
controls.closePopout();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleUploadCaption(e: ChangeEvent<HTMLInputElement>) {
|
|
||||||
if (!e.target.files) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const captionFile = e.target.files[0];
|
|
||||||
setCustomCaption(captionFile);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<FloatingView
|
<FloatingView
|
||||||
{...props.router.pageProps(props.prefix)}
|
{...props.router.pageProps(props.prefix)}
|
||||||
|
@ -105,23 +88,28 @@ export function CaptionSelectionPopout(props: {
|
||||||
{t("videoPlayer.popouts.noCaptions")}
|
{t("videoPlayer.popouts.noCaptions")}
|
||||||
</PopoutListEntry>
|
</PopoutListEntry>
|
||||||
<PopoutListEntry
|
<PopoutListEntry
|
||||||
key={CUSTOM_CAPTION_ID}
|
key={customCaption}
|
||||||
active={currentCaption === CUSTOM_CAPTION_ID}
|
active={currentCaption === customCaption}
|
||||||
loading={loadingCustomCaption}
|
loading={loading && loadingId.current === customCaption}
|
||||||
errored={!!errorCustomCaption}
|
errored={error && loadingId.current === customCaption}
|
||||||
onClick={() => {
|
onClick={() => customCaptionUploadElement.current?.click()}
|
||||||
customCaptionUploadElement.current?.click();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{currentCaption === CUSTOM_CAPTION_ID
|
{currentCaption === customCaption
|
||||||
? t("videoPlayer.popouts.customCaption")
|
? t("videoPlayer.popouts.customCaption")
|
||||||
: t("videoPlayer.popouts.uploadCustomCaption")}
|
: t("videoPlayer.popouts.uploadCustomCaption")}
|
||||||
<input
|
<input
|
||||||
ref={customCaptionUploadElement}
|
|
||||||
type="file"
|
|
||||||
onChange={handleUploadCaption}
|
|
||||||
className="hidden"
|
className="hidden"
|
||||||
accept=".vtt, .srt"
|
ref={customCaptionUploadElement}
|
||||||
|
accept={subtitleTypeList}
|
||||||
|
type="file"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e.target.files) return;
|
||||||
|
const customSubtitle = {
|
||||||
|
langIso: "custom",
|
||||||
|
url: URL.createObjectURL(e.target.files[0]),
|
||||||
|
};
|
||||||
|
setCaption(customSubtitle, false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</PopoutListEntry>
|
</PopoutListEntry>
|
||||||
</PopoutSection>
|
</PopoutSection>
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"baseUrl": "./src",
|
"baseUrl": "./src",
|
||||||
"typeRoots": ["./src/@types"],
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
},
|
},
|
||||||
|
|
22
yarn.lock
22
yarn.lock
|
@ -2123,11 +2123,6 @@ commander@^2.20.0:
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||||
|
|
||||||
commander@^7.1.0:
|
|
||||||
version "7.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
|
||||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
|
||||||
|
|
||||||
commander@^8.0.0:
|
commander@^8.0.0:
|
||||||
version "8.3.0"
|
version "8.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
|
||||||
|
@ -3854,13 +3849,6 @@ node-releases@^2.0.8:
|
||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
|
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
|
||||||
integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
|
integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
|
||||||
|
|
||||||
node-webvtt@^1.9.4:
|
|
||||||
version "1.9.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/node-webvtt/-/node-webvtt-1.9.4.tgz#b71b98f879c6c88ebeda40c358bd45a882ca5d89"
|
|
||||||
integrity sha512-EjrJdKdxSyd8j4LMLW6s2Ah4yNoeVXp18Ob04CQl1In18xcUmKzEE8pcsxxnFVqanTyjbGYph2VnvtwIXR4EjA==
|
|
||||||
dependencies:
|
|
||||||
commander "^7.1.0"
|
|
||||||
|
|
||||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||||
|
@ -4699,11 +4687,6 @@ sourcemap-codec@^1.4.8:
|
||||||
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
||||||
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
||||||
|
|
||||||
srt-webvtt@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/srt-webvtt/-/srt-webvtt-2.0.0.tgz#debd2f56dd2b6600894caa11bb78893e5fc6509b"
|
|
||||||
integrity sha512-G2Z7/Jf2NRKrmLYNSIhSYZZYE6OFlKXFp9Au2/zJBKgrioUzmrAys1x7GT01dwl6d2sEnqr5uahEIOd0JW/Rbw==
|
|
||||||
|
|
||||||
stack-generator@^2.0.5:
|
stack-generator@^2.0.5:
|
||||||
version "2.0.10"
|
version "2.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.10.tgz#8ae171e985ed62287d4f1ed55a1633b3fb53bb4d"
|
resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.10.tgz#8ae171e985ed62287d4f1ed55a1633b3fb53bb4d"
|
||||||
|
@ -4859,6 +4842,11 @@ subscribe-ui-event@^2.0.6:
|
||||||
lodash "^4.17.15"
|
lodash "^4.17.15"
|
||||||
raf "^3.0.0"
|
raf "^3.0.0"
|
||||||
|
|
||||||
|
subsrt-ts@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/subsrt-ts/-/subsrt-ts-2.1.0.tgz#97b5e0f97800fb08b64465b53c7c4f14f43d6fd4"
|
||||||
|
integrity sha512-LOdp6A91l/yPLPFuEaYvGzFDusUz0J52ksZjaCFdl347DOhedZOVQEciTaH7KaVDRlb7wstOx4dPFdjf9AyuFw==
|
||||||
|
|
||||||
supports-color@^5.3.0:
|
supports-color@^5.3.0:
|
||||||
version "5.5.0"
|
version "5.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||||
|
|
Loading…
Reference in a new issue