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

@remotion/media-parser: Parse transport stream (.ts) files #4617

Merged
merged 30 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion packages/convert/app/components/ContainerOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {formatBytes} from '~/lib/format-bytes';
import {formatSeconds} from '~/lib/format-seconds';
import {
renderHumanReadableAudioCodec,
renderHumanReadableContainer,
renderHumanReadableVideoCodec,
} from '~/lib/render-codec-label';
import {MetadataDisplay} from './MetadataTable';
Expand Down Expand Up @@ -54,7 +55,7 @@ export const ContainerOverview: React.FC<{
<TableCell className="font-brand">Container</TableCell>
<TableCell className="text-right">
{container ? (
<>{String(container)}</>
<>{renderHumanReadableContainer(container)}</>
) : (
<Skeleton className="h-3 w-[100px] inline-block" />
)}
Expand Down
22 changes: 14 additions & 8 deletions packages/convert/app/components/ConvertUi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
MediaParserAudioCodec,
MediaParserInternals,
MediaParserVideoCodec,
ParseMediaContainer,
TracksField,
} from '@remotion/media-parser';
import {fetchReader} from '@remotion/media-parser/fetch';
Expand Down Expand Up @@ -53,13 +54,15 @@ export default function ConvertUI({
flipVertical,
setFlipHorizontal,
setFlipVertical,
inputContainer,
}: {
readonly src: Source;
readonly setSrc: React.Dispatch<React.SetStateAction<Source | null>>;
readonly currentAudioCodec: MediaParserAudioCodec | null;
readonly currentVideoCodec: MediaParserVideoCodec | null;
readonly tracks: TracksField | null;
readonly duration: number | null;
readonly inputContainer: ParseMediaContainer | null;
readonly logLevel: LogLevel;
readonly action: RouteAction;
readonly enableRotateOrMirror: RotateOrMirrorState;
Expand All @@ -73,7 +76,7 @@ export default function ConvertUI({
readonly setFlipHorizontal: React.Dispatch<React.SetStateAction<boolean>>;
readonly setFlipVertical: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const [container, setContainer] = useState<ConvertMediaContainer>(() =>
const [outputContainer, setContainer] = useState<ConvertMediaContainer>(() =>
getDefaultContainerForConversion(src, action),
);
const [videoConfigIndexSelection, _setVideoConfigIndex] = useState<
Expand All @@ -97,10 +100,11 @@ export default function ConvertUI({
}, [action]);

const supportedConfigs = useSupportedConfigs({
container,
outputContainer,
tracks,
action,
userRotation,
inputContainer,
});

const setVideoConfigIndex = useCallback((trackId: number, i: number) => {
Expand Down Expand Up @@ -153,7 +157,7 @@ export default function ConvertUI({
},
});
},
container,
container: outputContainer,
signal: abortController.signal,
fields: {
name: true,
Expand Down Expand Up @@ -238,7 +242,7 @@ export default function ConvertUI({
src,
userRotation,
logLevel,
container,
outputContainer,
flipHorizontal,
enableRotateOrMirror,
flipVertical,
Expand Down Expand Up @@ -305,7 +309,7 @@ export default function ConvertUI({
<ConvertProgress
state={state.state}
name={name}
container={container}
container={outputContainer}
done={false}
duration={duration}
isReencoding={
Expand All @@ -332,7 +336,7 @@ export default function ConvertUI({
done
state={state.state}
name={name}
container={container}
container={outputContainer}
duration={duration}
isReencoding={
supportedConfigs !== null &&
Expand All @@ -344,7 +348,9 @@ export default function ConvertUI({
}
/>
<div className="h-2" />
<ConversionDone {...{container, name, setState, state, setSrc}} />
<ConversionDone
{...{container: outputContainer, name, setState, state, setSrc}}
/>
</>
);
}
Expand Down Expand Up @@ -381,7 +387,7 @@ export default function ConvertUI({
<div className="h-2" />
<ConvertForm
{...{
container,
container: outputContainer,
setContainer,
flipHorizontal,
flipVertical,
Expand Down
7 changes: 5 additions & 2 deletions packages/convert/app/components/FileAvailable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import React, {useCallback, useMemo, useRef, useState} from 'react';
import {Source} from '~/lib/convert-state';
import {defaultRotateOrMirorState, RotateOrMirrorState} from '~/lib/default-ui';
import {formatBytes} from '~/lib/format-bytes';
import {useThumbnail} from '~/lib/use-thumbnail';
import {RouteAction} from '~/seo';
import ConvertUI from './ConvertUi';
import {Footer} from './Footer';
import {Probe} from './Probe';
import {VideoThumbnailRef} from './VideoThumbnail';
import {Button} from './ui/button';
import {useProbe, useThumbnail} from './use-probe';
import {useProbe} from './use-probe';

export const FileAvailable: React.FC<{
readonly src: Source;
Expand Down Expand Up @@ -53,7 +54,7 @@ export const FileAvailable: React.FC<{
const [enableRotateOrMirrow, setEnableRotateOrMirror] =
useState<RotateOrMirrorState>(() => defaultRotateOrMirorState(routeAction));

useThumbnail({src, logLevel: 'verbose', onVideoThumbnail});
const {err} = useThumbnail({src, logLevel: 'verbose', onVideoThumbnail});
const probeResult = useProbe({
src,
logLevel: 'verbose',
Expand Down Expand Up @@ -94,6 +95,7 @@ export const FileAvailable: React.FC<{
</div>
<div className="lg:inline-flex lg:flex-row">
<Probe
thumbnailError={err}
src={src}
probeDetails={probeDetails}
setProbeDetails={setProbeDetails}
Expand All @@ -114,6 +116,7 @@ export const FileAvailable: React.FC<{
>
<div className="gap-4">
<ConvertUI
inputContainer={probeResult.container}
currentAudioCodec={probeResult.audioCodec ?? null}
currentVideoCodec={probeResult.videoCodec ?? null}
src={src}
Expand Down
2 changes: 1 addition & 1 deletion packages/convert/app/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const Footer: React.FC = () => {
href="https://github.com/remotion-dev/remotion/tree/main/packages/convert"
className="text-sm text-muted-foreground font-medium hover:text-foreground"
>
Source code
Source Code
</a>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/convert/app/components/MetadataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const MetadataDisplay: React.FC<{
return (
<>
{filtered.map((entry) => (
<TableRow>
<TableRow key={entry.key}>
<TableCell className="font-brand">
<LimitedWidthLabel alt={entry.key}>
{renderMetadataLabel(entry.key)}
Expand Down
4 changes: 3 additions & 1 deletion packages/convert/app/components/Probe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const Probe: React.FC<{
readonly userRotation: number;
readonly mirrorHorizontal: boolean;
readonly mirrorVertical: boolean;
readonly thumbnailError: Error | null;
}> = ({
src,
probeDetails,
Expand All @@ -32,6 +33,7 @@ export const Probe: React.FC<{
userRotation,
mirrorHorizontal,
mirrorVertical,
thumbnailError,
}) => {
const {
audioCodec,
Expand Down Expand Up @@ -80,7 +82,7 @@ export const Probe: React.FC<{
return (
<Card className="w-full lg:w-[350px] overflow-hidden">
<div className="flex flex-row lg:flex-col w-full border-b-2 border-black">
{error ? null : (
{error ? null : thumbnailError ? null : (
<VideoThumbnail
ref={videoThumbnailRef}
smallThumbOnMobile
Expand Down
10 changes: 7 additions & 3 deletions packages/convert/app/components/get-supported-configs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {TracksField} from '@remotion/media-parser';
import {ParseMediaContainer, TracksField} from '@remotion/media-parser';
import {
AudioOperation,
canCopyAudioTrack,
Expand Down Expand Up @@ -59,12 +59,14 @@ export const getSupportedConfigs = async ({
bitrate,
action,
userRotation,
inputContainer,
}: {
tracks: TracksField;
container: ConvertMediaContainer;
bitrate: number;
action: RouteAction;
userRotation: number;
inputContainer: ParseMediaContainer;
}): Promise<SupportedConfigs> => {
const availableVideoCodecs = getAvailableVideoCodecs({container});

Expand All @@ -77,9 +79,10 @@ export const getSupportedConfigs = async ({
const options: VideoOperation[] = [];
const canCopy = canCopyVideoTrack({
inputCodec: track.codecWithoutConfig,
container,
outputContainer: container,
inputRotation: track.rotation,
rotationToApply: userRotation,
inputContainer,
});
if (canCopy && prioritizeCopyOverReencode) {
options.push({
Expand Down Expand Up @@ -120,7 +123,8 @@ export const getSupportedConfigs = async ({

const canCopy = canCopyAudioTrack({
inputCodec: track.codecWithoutConfig,
container,
outputContainer: container,
inputContainer,
});

if (canCopy) {
Expand Down
13 changes: 12 additions & 1 deletion packages/convert/app/components/guess-codec-from-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ import {ConvertMediaContainer} from '@remotion/webcodecs';
import {Source} from '~/lib/convert-state';
import {RouteAction} from '~/seo';

const guessFromExtension = (src: string) => {
const guessFromExtension = (src: string): ParseMediaContainer => {
if (src.endsWith('.webm')) {
return 'webm';
}

if (src.endsWith('.mkv')) {
return 'webm';
}

if (src.endsWith('.avi')) {
return 'avi';
}

if (src.endsWith('.ts')) {
return 'transport-stream';
}

return 'mp4';
};

Expand Down Expand Up @@ -75,5 +82,9 @@ export const getDefaultContainerForConversion = (
return keepSame ? 'webm' : 'mp4';
}

if (guessed === 'transport-stream') {
return 'mp4';
}

throw new Error('Unhandled container ' + (guessed satisfies never));
};
65 changes: 0 additions & 65 deletions packages/convert/app/components/use-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,71 +16,6 @@ import {Source} from '~/lib/convert-state';

export type ProbeResult = ReturnType<typeof useProbe>;

export const useThumbnail = ({
src,
logLevel,
onVideoThumbnail,
}: {
src: Source;
logLevel: LogLevel;
onVideoThumbnail: (videoFrame: VideoFrame) => void;
}) => {
const abortController = new AbortController();
parseMedia({
signal: abortController.signal,
reader: src.type === 'file' ? webFileReader : fetchReader,
src: src.type === 'file' ? src.file : src.url,
logLevel,
onVideoTrack: async (track) => {
if (typeof VideoDecoder === 'undefined') {
return null;
}

let frames = 0;

const decoder = new VideoDecoder({
error: (error) => {
// eslint-disable-next-line no-console
console.log(error);
},
output(frame) {
onVideoThumbnail(frame);
frame.close();
},
});

if (!(await VideoDecoder.isConfigSupported(track)).supported) {
return null;
}

// TODO: See if possible
decoder.configure(track);
return (sample) => {
frames++;
if (frames >= 30) {
abortController.abort();
}

decoder.decode(new EncodedVideoChunk(sample));
};
},
}).catch((err) => {
if ((err as Error).stack?.includes('Cancelled')) {
return;
}
if ((err as Error).stack?.toLowerCase()?.includes('aborted')) {
return;
}
// firefox
if ((err as Error).message?.toLowerCase()?.includes('aborted')) {
return;
}

// eslint-disable-next-line no-console
console.log(err);
});
};

export const useProbe = ({
src,
logLevel,
Expand Down
Loading
Loading