mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
what am i doing
This commit is contained in:
+1689
-1765
File diff suppressed because it is too large
Load Diff
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"encryption.key":"6csmILMKXW5vUdcKm142jEES+lg717F52EbuARHN44o=","encryption.expire_at":1769323998372}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"type": "commonjs"}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"/editor/[project_id]/page": "app/editor/[project_id]/page.js"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"6csmILMKXW5vUdcKm142jEES+lg717F52EbuARHN44o=\"\n}"
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"node": {},
|
|
||||||
"edge": {},
|
|
||||||
"encryptionKey": "6csmILMKXW5vUdcKm142jEES+lg717F52EbuARHN44o="
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
Vendored
-90
@@ -1,90 +0,0 @@
|
|||||||
// This file is generated automatically by Next.js
|
|
||||||
// Do not edit this file manually
|
|
||||||
|
|
||||||
type AppRoutes = "/" | "/blog" | "/blog/[slug]" | "/contributors" | "/editor/[project_id]" | "/privacy" | "/projects" | "/roadmap" | "/sponsors" | "/terms"
|
|
||||||
type AppRouteHandlerRoutes = "/api/auth/[...all]" | "/api/get-upload-url" | "/api/health" | "/api/sounds/search" | "/api/transcribe" | "/rss.xml"
|
|
||||||
type PageRoutes = never
|
|
||||||
type LayoutRoutes = "/" | "/editor/[project_id]"
|
|
||||||
type RedirectRoutes = never
|
|
||||||
type RewriteRoutes = "/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/[[...path]]" | "/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/a-4-a/c.js"
|
|
||||||
type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes | AppRouteHandlerRoutes
|
|
||||||
|
|
||||||
|
|
||||||
interface ParamMap {
|
|
||||||
"/": {}
|
|
||||||
"/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/[[...path]]": { "path"?: string[]; }
|
|
||||||
"/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/a-4-a/c.js": {}
|
|
||||||
"/api/auth/[...all]": { "all": string[]; }
|
|
||||||
"/api/get-upload-url": {}
|
|
||||||
"/api/health": {}
|
|
||||||
"/api/sounds/search": {}
|
|
||||||
"/api/transcribe": {}
|
|
||||||
"/blog": {}
|
|
||||||
"/blog/[slug]": { "slug": string; }
|
|
||||||
"/contributors": {}
|
|
||||||
"/editor/[project_id]": { "project_id": string; }
|
|
||||||
"/privacy": {}
|
|
||||||
"/projects": {}
|
|
||||||
"/roadmap": {}
|
|
||||||
"/rss.xml": {}
|
|
||||||
"/sponsors": {}
|
|
||||||
"/terms": {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export type ParamsOf<Route extends Routes> = ParamMap[Route]
|
|
||||||
|
|
||||||
interface LayoutSlotMap {
|
|
||||||
"/": never
|
|
||||||
"/editor/[project_id]": never
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes, ParamMap, AppRouteHandlerRoutes }
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
/**
|
|
||||||
* Props for Next.js App Router page components
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* export default function Page(props: PageProps<'/blog/[slug]'>) {
|
|
||||||
* const { slug } = await props.params
|
|
||||||
* return <div>Blog post: {slug}</div>
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
interface PageProps<AppRoute extends AppRoutes> {
|
|
||||||
params: Promise<ParamMap[AppRoute]>
|
|
||||||
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for Next.js App Router layout components
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* export default function Layout(props: LayoutProps<'/dashboard'>) {
|
|
||||||
* return <div>{props.children}</div>
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
type LayoutProps<LayoutRoute extends LayoutRoutes> = {
|
|
||||||
params: Promise<ParamMap[LayoutRoute]>
|
|
||||||
children: React.ReactNode
|
|
||||||
} & {
|
|
||||||
[K in LayoutSlotMap[LayoutRoute]]: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Context for Next.js App Router route handlers
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* export async function GET(request: NextRequest, context: RouteContext<'/api/users/[id]'>) {
|
|
||||||
* const { id } = await context.params
|
|
||||||
* return Response.json({ id })
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
interface RouteContext<AppRouteHandlerRoute extends AppRouteHandlerRoutes> {
|
|
||||||
params: Promise<ParamMap[AppRouteHandlerRoute]>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"feed": "^5.1.0",
|
"feed": "^5.1.0",
|
||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"mediabunny": "^1.9.3",
|
"mediabunny": "^1.29.1",
|
||||||
"motion": "^12.18.1",
|
"motion": "^12.18.1",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "16.1.3",
|
"next": "16.1.3",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import type { KeyboardEvent, MouseEvent } from "react";
|
import type { KeyboardEvent, MouseEvent } from "react";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { DeleteProjectDialog } from "@/components/delete-project-dialog";
|
import { DeleteProjectDialog } from "@/components/editor/delete-project-dialog";
|
||||||
import { RenameProjectDialog } from "@/components/rename-project-dialog";
|
import { RenameProjectDialog } from "@/components/rename-project-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
@@ -419,14 +419,12 @@ function ProjectCard({
|
|||||||
|
|
||||||
const cardContent = (
|
const cardContent = (
|
||||||
<Card
|
<Card
|
||||||
className={`bg-background overflow-hidden border-none p-0 transition-all ${
|
className={`bg-background overflow-hidden border-none p-0 transition-all ${isSelectionMode && isSelected ? "ring-primary ring-2" : ""
|
||||||
isSelectionMode && isSelected ? "ring-primary ring-2" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`bg-muted relative aspect-square transition-opacity ${
|
className={`bg-muted relative aspect-square transition-opacity ${isDropdownOpen ? "opacity-65" : "opacity-100 group-hover:opacity-65"
|
||||||
isDropdownOpen ? "opacity-65" : "opacity-100 group-hover:opacity-65"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{isSelectionMode && (
|
{isSelectionMode && (
|
||||||
<div className="absolute top-3 left-3 z-10">
|
<div className="absolute top-3 left-3 z-10">
|
||||||
@@ -477,11 +475,10 @@ function ProjectCard({
|
|||||||
aria-label="project options"
|
aria-label="project options"
|
||||||
variant="text"
|
variant="text"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={`ml-2 size-6 shrink-0 p-0 transition-all ${
|
className={`ml-2 size-6 shrink-0 p-0 transition-all ${isDropdownOpen
|
||||||
isDropdownOpen
|
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0 group-hover:opacity-100"
|
: "opacity-0 group-hover:opacity-100"
|
||||||
}`}
|
}`}
|
||||||
onClick={(event) => event.preventDefault()}
|
onClick={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal aria-hidden="true" />
|
<MoreHorizontal aria-hidden="true" />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Loader2 } from "lucide-react";
|
|||||||
import type { TranscriptionProgress } from "@/types/transcription";
|
import type { TranscriptionProgress } from "@/types/transcription";
|
||||||
import { transcriptionService } from "@/services/transcription";
|
import { transcriptionService } from "@/services/transcription";
|
||||||
import { decodeAudioToFloat32 } from "@/lib/media/audio";
|
import { decodeAudioToFloat32 } from "@/lib/media/audio";
|
||||||
import { buildCaptionChunks } from "@/lib/utils/caption-utils";
|
import { buildCaptionChunks } from "@/lib/transcription/caption";
|
||||||
|
|
||||||
export function Captions() {
|
export function Captions() {
|
||||||
const [selectedLanguage, setSelectedLanguage] = useState("auto");
|
const [selectedLanguage, setSelectedLanguage] = useState("auto");
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { DraggableItem } from "@/components/ui/draggable-item";
|
import { DraggableItem } from "@/components/editor/draggable-item";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
} from "@/lib/iconify-api";
|
} from "@/lib/iconify-api";
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { DraggableItem } from "@/components/ui/draggable-item";
|
import { DraggableItem } from "@/components/editor/draggable-item";
|
||||||
import { InputWithBack } from "@/components/ui/input-with-back";
|
import { InputWithBack } from "@/components/ui/input-with-back";
|
||||||
import type { StickerCategory } from "@/types/stickers";
|
import type { StickerCategory } from "@/types/stickers";
|
||||||
import { STICKER_CATEGORIES } from "@/constants/stickers-constants";
|
import { STICKER_CATEGORIES } from "@/constants/stickers-constants";
|
||||||
@@ -208,17 +208,17 @@ function StickersContentView({ category }: { category: StickerCategory }) {
|
|||||||
const collection = collections[c.prefix];
|
const collection = collections[c.prefix];
|
||||||
return collection
|
return collection
|
||||||
? {
|
? {
|
||||||
prefix: c.prefix,
|
prefix: c.prefix,
|
||||||
name: c.name,
|
name: c.name,
|
||||||
total: collection.total,
|
total: collection.total,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
})
|
})
|
||||||
.filter(Boolean) as Array<{
|
.filter(Boolean) as Array<{
|
||||||
prefix: string;
|
prefix: string;
|
||||||
name: string;
|
name: string;
|
||||||
total: number;
|
total: number;
|
||||||
}>;
|
}>;
|
||||||
}, [collections, category]);
|
}, [collections, category]);
|
||||||
|
|
||||||
const { scrollAreaRef, handleScroll } = useInfiniteScroll({
|
const { scrollAreaRef, handleScroll } = useInfiniteScroll({
|
||||||
@@ -531,10 +531,10 @@ function StickerItem({
|
|||||||
hostIndex === 0
|
hostIndex === 0
|
||||||
? getIconSvgUrl(iconName, { width: 64, height: 64 })
|
? getIconSvgUrl(iconName, { width: 64, height: 64 })
|
||||||
: buildIconSvgUrl(
|
: buildIconSvgUrl(
|
||||||
ICONIFY_HOSTS[Math.min(hostIndex, ICONIFY_HOSTS.length - 1)],
|
ICONIFY_HOSTS[Math.min(hostIndex, ICONIFY_HOSTS.length - 1)],
|
||||||
iconName,
|
iconName,
|
||||||
{ width: 64, height: 64 },
|
{ width: 64, height: 64 },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
alt={displayName}
|
alt={displayName}
|
||||||
width={64}
|
width={64}
|
||||||
@@ -543,9 +543,9 @@ function StickerItem({
|
|||||||
style={
|
style={
|
||||||
capSize
|
capSize
|
||||||
? {
|
? {
|
||||||
maxWidth: "var(--sticker-max, 160px)",
|
maxWidth: "var(--sticker-max, 160px)",
|
||||||
maxHeight: "var(--sticker-max, 160px)",
|
maxHeight: "var(--sticker-max, 160px)",
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DraggableItem } from "@/components/ui/draggable-item";
|
import { DraggableItem } from "@/components/editor/draggable-item";
|
||||||
import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view";
|
import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
|
import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from "../ui/dropdown-menu";
|
} from "../ui/dropdown-menu";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { RenameProjectDialog } from "../rename-project-dialog";
|
import { RenameProjectDialog } from "../rename-project-dialog";
|
||||||
import { DeleteProjectDialog } from "../delete-project-dialog";
|
import { DeleteProjectDialog } from "./delete-project-dialog";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { FaDiscord } from "react-icons/fa6";
|
import { FaDiscord } from "react-icons/fa6";
|
||||||
import { ExportButton } from "./export-button";
|
import { ExportButton } from "./export-button";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
|
|||||||
import { Progress } from "../ui/progress";
|
import { Progress } from "../ui/progress";
|
||||||
import { Checkbox } from "../ui/checkbox";
|
import { Checkbox } from "../ui/checkbox";
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
import { getExportMimeType, getExportFileExtension } from "@/utils/export";
|
import { getExportMimeType, getExportFileExtension } from "@/lib/export";
|
||||||
import { Check, Copy, Download, RotateCcw, X } from "lucide-react";
|
import { Check, Copy, Download, RotateCcw, X } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
EXPORT_FORMAT_VALUES,
|
EXPORT_FORMAT_VALUES,
|
||||||
@@ -108,7 +108,6 @@ function ExportPopover({
|
|||||||
setExportResult(result);
|
setExportResult(result);
|
||||||
|
|
||||||
if (result.success && result.buffer) {
|
if (result.success && result.buffer) {
|
||||||
// Download the file
|
|
||||||
const mimeType = getExportMimeType({ format });
|
const mimeType = getExportMimeType({ format });
|
||||||
const extension = getExportFileExtension({ format });
|
const extension = getExportFileExtension({ format });
|
||||||
const blob = new Blob([result.buffer], { type: mimeType });
|
const blob = new Blob([result.buffer], { type: mimeType });
|
||||||
|
|||||||
@@ -36,13 +36,12 @@ import {
|
|||||||
getTotalTracksHeight,
|
getTotalTracksHeight,
|
||||||
canTracktHaveAudio,
|
canTracktHaveAudio,
|
||||||
canTrackBeHidden,
|
canTrackBeHidden,
|
||||||
isMainTrack,
|
|
||||||
getTimelineZoomMin,
|
getTimelineZoomMin,
|
||||||
} from "@/lib/timeline";
|
} from "@/lib/timeline";
|
||||||
import { TimelineToolbar } from "./timeline-toolbar";
|
import { TimelineToolbar } from "./timeline-toolbar";
|
||||||
import { useScrollSync } from "@/hooks/timeline/use-scroll-sync";
|
import { useScrollSync } from "@/hooks/timeline/use-scroll-sync";
|
||||||
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
|
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
|
||||||
import { useTimelineInteractions } from "@/hooks/timeline/use-timeline-interactions";
|
import { useTimelineSeek } from "@/hooks/timeline/use-timeline-seek";
|
||||||
import { useTimelineDragDrop } from "@/hooks/timeline/use-timeline-drag-drop";
|
import { useTimelineDragDrop } from "@/hooks/timeline/use-timeline-drag-drop";
|
||||||
import { TimelineRuler } from "./timeline-ruler";
|
import { TimelineRuler } from "./timeline-ruler";
|
||||||
import { TimelineBookmarksRow } from "./bookmarks";
|
import { TimelineBookmarksRow } from "./bookmarks";
|
||||||
@@ -162,7 +161,7 @@ export function Timeline() {
|
|||||||
handleTracksClick,
|
handleTracksClick,
|
||||||
handleRulerMouseDown,
|
handleRulerMouseDown,
|
||||||
handleRulerClick,
|
handleRulerClick,
|
||||||
} = useTimelineInteractions({
|
} = useTimelineSeek({
|
||||||
playheadRef,
|
playheadRef,
|
||||||
trackLabelsRef,
|
trackLabelsRef,
|
||||||
rulerScrollRef,
|
rulerScrollRef,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { formatTimeCode } from "@/lib/time";
|
import { formatTimeCode } from "@/lib/time";
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
import { EditableTimecode } from "@/components/ui/editable-timecode";
|
import { EditableTimecode } from "@/components/editor/editable-timecode";
|
||||||
import { ScenesView } from "../scenes-view";
|
import { ScenesView } from "../scenes-view";
|
||||||
import { type TAction, invokeAction } from "@/lib/actions";
|
import { type TAction, invokeAction } from "@/lib/actions";
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
|
||||||
import { useRef, useEffect } from "react";
|
|
||||||
|
|
||||||
interface AudioPlayerProps {
|
|
||||||
src: string;
|
|
||||||
className?: string;
|
|
||||||
clipStartTime: number;
|
|
||||||
trimStart: number;
|
|
||||||
trimEnd: number;
|
|
||||||
clipDuration: number;
|
|
||||||
trackMuted?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AudioPlayer({
|
|
||||||
src,
|
|
||||||
className = "",
|
|
||||||
clipStartTime,
|
|
||||||
trimStart,
|
|
||||||
trimEnd,
|
|
||||||
clipDuration,
|
|
||||||
trackMuted = false,
|
|
||||||
}: AudioPlayerProps) {
|
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
|
||||||
const editor = useEditor();
|
|
||||||
const currentTime = editor.playback.getCurrentTime();
|
|
||||||
const isPlaying = editor.playback.getIsPlaying();
|
|
||||||
const volume = editor.playback.getVolume();
|
|
||||||
const muted = editor.playback.isMuted();
|
|
||||||
const speed = editor.playback.getSpeed();
|
|
||||||
|
|
||||||
// Calculate if we're within this clip's timeline range
|
|
||||||
const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd);
|
|
||||||
const isInClipRange =
|
|
||||||
currentTime >= clipStartTime && currentTime < clipEndTime;
|
|
||||||
|
|
||||||
// Sync playback events
|
|
||||||
useEffect(() => {
|
|
||||||
const audio = audioRef.current;
|
|
||||||
if (!audio || !isInClipRange) return;
|
|
||||||
|
|
||||||
const handleSeekEvent = (e: CustomEvent) => {
|
|
||||||
// Always update audio time, even if outside clip range
|
|
||||||
const timelineTime = e.detail.time;
|
|
||||||
const audioTime = Math.max(
|
|
||||||
trimStart,
|
|
||||||
Math.min(
|
|
||||||
clipDuration - trimEnd,
|
|
||||||
timelineTime - clipStartTime + trimStart,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
audio.currentTime = audioTime;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateEvent = (e: CustomEvent) => {
|
|
||||||
// Always update audio time, even if outside clip range
|
|
||||||
const timelineTime = e.detail.time;
|
|
||||||
const targetTime = Math.max(
|
|
||||||
trimStart,
|
|
||||||
Math.min(
|
|
||||||
clipDuration - trimEnd,
|
|
||||||
timelineTime - clipStartTime + trimStart,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Math.abs(audio.currentTime - targetTime) > 0.5) {
|
|
||||||
audio.currentTime = targetTime;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSpeed = (e: CustomEvent) => {
|
|
||||||
audio.playbackRate = e.detail.speed;
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("playback-seek", handleSeekEvent as EventListener);
|
|
||||||
window.addEventListener(
|
|
||||||
"playback-update",
|
|
||||||
handleUpdateEvent as EventListener,
|
|
||||||
);
|
|
||||||
window.addEventListener("playback-speed", handleSpeed as EventListener);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener(
|
|
||||||
"playback-seek",
|
|
||||||
handleSeekEvent as EventListener,
|
|
||||||
);
|
|
||||||
window.removeEventListener(
|
|
||||||
"playback-update",
|
|
||||||
handleUpdateEvent as EventListener,
|
|
||||||
);
|
|
||||||
window.removeEventListener(
|
|
||||||
"playback-speed",
|
|
||||||
handleSpeed as EventListener,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]);
|
|
||||||
|
|
||||||
// Sync playback state
|
|
||||||
useEffect(() => {
|
|
||||||
const audio = audioRef.current;
|
|
||||||
if (!audio) return;
|
|
||||||
|
|
||||||
if (isPlaying && isInClipRange && !trackMuted) {
|
|
||||||
audio.play().catch(() => {});
|
|
||||||
} else {
|
|
||||||
audio.pause();
|
|
||||||
}
|
|
||||||
}, [isPlaying, isInClipRange, trackMuted]);
|
|
||||||
|
|
||||||
// Sync volume and speed
|
|
||||||
useEffect(() => {
|
|
||||||
const audio = audioRef.current;
|
|
||||||
if (!audio) return;
|
|
||||||
|
|
||||||
audio.volume = volume;
|
|
||||||
audio.muted = muted || trackMuted;
|
|
||||||
audio.playbackRate = speed;
|
|
||||||
}, [volume, speed, muted, trackMuted]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<audio
|
|
||||||
ref={audioRef}
|
|
||||||
src={src}
|
|
||||||
className={className}
|
|
||||||
preload="auto"
|
|
||||||
controls={false}
|
|
||||||
style={{ display: "none" }} // Audio elements don't need visual representation
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,3 +5,8 @@ export const DEFAULT_EXPORT_OPTIONS = {
|
|||||||
quality: "high",
|
quality: "high",
|
||||||
includeAudio: true,
|
includeAudio: true,
|
||||||
} satisfies ExportOptions;
|
} satisfies ExportOptions;
|
||||||
|
|
||||||
|
export const EXPORT_MIME_TYPES = {
|
||||||
|
webm: "video/webm",
|
||||||
|
mp4: "video/mp4",
|
||||||
|
} as const;
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@ import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
|||||||
import { getSnappedSeekTime } from "@/lib/time";
|
import { getSnappedSeekTime } from "@/lib/time";
|
||||||
import { useEditor } from "../use-editor";
|
import { useEditor } from "../use-editor";
|
||||||
|
|
||||||
interface UseTimelineInteractionsProps {
|
interface UseTimelineSeekProps {
|
||||||
playheadRef: RefObject<HTMLDivElement>;
|
playheadRef: RefObject<HTMLDivElement>;
|
||||||
trackLabelsRef: RefObject<HTMLDivElement>;
|
trackLabelsRef: RefObject<HTMLDivElement>;
|
||||||
rulerScrollRef: RefObject<HTMLDivElement>;
|
rulerScrollRef: RefObject<HTMLDivElement>;
|
||||||
@@ -54,7 +54,7 @@ function setMouseTracking({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTimelineInteractions({
|
export function useTimelineSeek({
|
||||||
playheadRef,
|
playheadRef,
|
||||||
trackLabelsRef,
|
trackLabelsRef,
|
||||||
rulerScrollRef,
|
rulerScrollRef,
|
||||||
@@ -64,7 +64,7 @@ export function useTimelineInteractions({
|
|||||||
isSelecting,
|
isSelecting,
|
||||||
clearSelectedElements,
|
clearSelectedElements,
|
||||||
seek,
|
seek,
|
||||||
}: UseTimelineInteractionsProps) {
|
}: UseTimelineSeekProps) {
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
const activeProject = editor.project.getActive();
|
const activeProject = editor.project.getActive();
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { EXPORT_MIME_TYPES } from "@/constants/export-constants";
|
||||||
import { ExportFormat } from "@/types/export";
|
import { ExportFormat } from "@/types/export";
|
||||||
|
|
||||||
export function getExportMimeType({
|
export function getExportMimeType({
|
||||||
@@ -5,12 +6,7 @@ export function getExportMimeType({
|
|||||||
}: {
|
}: {
|
||||||
format: ExportFormat;
|
format: ExportFormat;
|
||||||
}): string {
|
}): string {
|
||||||
const mimeTypes: Record<ExportFormat, string> = {
|
return EXPORT_MIME_TYPES[format];
|
||||||
webm: "video/webm",
|
|
||||||
mp4: "video/mp4",
|
|
||||||
};
|
|
||||||
|
|
||||||
return mimeTypes[format];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExportFileExtension({
|
export function getExportFileExtension({
|
||||||
@@ -1,20 +1,8 @@
|
|||||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
import { Input, ALL_FORMATS, BlobSource, AudioBufferSink } from "mediabunny";
|
||||||
import { Input, ALL_FORMATS, BlobSource } from "mediabunny";
|
|
||||||
import { collectAudioMixSources } from "@/lib/media/audio";
|
import { collectAudioMixSources } from "@/lib/media/audio";
|
||||||
import type { TimelineTrack } from "@/types/timeline";
|
import type { TimelineTrack } from "@/types/timeline";
|
||||||
import type { MediaAsset } from "@/types/assets";
|
import type { MediaAsset } from "@/types/assets";
|
||||||
|
|
||||||
let ffmpeg: FFmpeg | null = null;
|
|
||||||
|
|
||||||
export const initFFmpeg = async (): Promise<FFmpeg> => {
|
|
||||||
if (ffmpeg) return ffmpeg;
|
|
||||||
|
|
||||||
ffmpeg = new FFmpeg();
|
|
||||||
await ffmpeg.load(); // Use default config
|
|
||||||
|
|
||||||
return ffmpeg;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getVideoInfo({
|
export async function getVideoInfo({
|
||||||
videoFile,
|
videoFile,
|
||||||
}: {
|
}: {
|
||||||
@@ -37,7 +25,6 @@ export async function getVideoInfo({
|
|||||||
throw new Error("No video track found in the file");
|
throw new Error("No video track found in the file");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get frame rate from packet statistics
|
|
||||||
const packetStats = await videoTrack.computePacketStats(100);
|
const packetStats = await videoTrack.computePacketStats(100);
|
||||||
const fps = packetStats.averagePacketRate;
|
const fps = packetStats.averagePacketRate;
|
||||||
|
|
||||||
@@ -49,8 +36,9 @@ export async function getVideoInfo({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// audio mixing for timeline - keeping ffmpeg for now due to complexity
|
const SAMPLE_RATE = 44100;
|
||||||
// TODO: Replace with Mediabunny audio processing when implementing canvas preview
|
const NUM_CHANNELS = 2;
|
||||||
|
|
||||||
export const extractTimelineAudio = async ({
|
export const extractTimelineAudio = async ({
|
||||||
tracks,
|
tracks,
|
||||||
mediaAssets,
|
mediaAssets,
|
||||||
@@ -62,24 +50,8 @@ export const extractTimelineAudio = async ({
|
|||||||
totalDuration: number;
|
totalDuration: number;
|
||||||
onProgress?: (progress: number) => void;
|
onProgress?: (progress: number) => void;
|
||||||
}): Promise<Blob> => {
|
}): Promise<Blob> => {
|
||||||
const ffmpeg = new FFmpeg();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await ffmpeg.load();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load fresh FFmpeg instance:", error);
|
|
||||||
throw new Error("Unable to initialize audio processing. Please try again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalDuration === 0) {
|
if (totalDuration === 0) {
|
||||||
const emptyAudioData = new ArrayBuffer(44);
|
return createWavBlob({ samples: new Float32Array(SAMPLE_RATE * 0.1) });
|
||||||
return new Blob([emptyAudioData], { type: "audio/wav" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onProgress) {
|
|
||||||
ffmpeg.on("progress", ({ progress }) => {
|
|
||||||
onProgress(progress * 100);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioMixSources = await collectAudioMixSources({
|
const audioMixSources = await collectAudioMixSources({
|
||||||
@@ -88,140 +60,159 @@ export const extractTimelineAudio = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (audioMixSources.length === 0) {
|
if (audioMixSources.length === 0) {
|
||||||
// Return silent audio if no audio elements
|
const silentDuration = Math.max(1, totalDuration);
|
||||||
const silentDuration = Math.max(1, totalDuration); // At least 1 second
|
const silentSamples = new Float32Array(
|
||||||
|
Math.ceil(silentDuration * SAMPLE_RATE) * NUM_CHANNELS,
|
||||||
|
);
|
||||||
|
return createWavBlob({ samples: silentSamples });
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSamples = Math.ceil(totalDuration * SAMPLE_RATE);
|
||||||
|
const mixBuffers = [
|
||||||
|
new Float32Array(totalSamples),
|
||||||
|
new Float32Array(totalSamples),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < audioMixSources.length; i++) {
|
||||||
|
const source = audioMixSources[i];
|
||||||
|
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress((i / audioMixSources.length) * 90);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const silentAudio = await generateSilentAudio(silentDuration);
|
await decodeAndMixAudioSource({
|
||||||
return silentAudio;
|
source,
|
||||||
|
mixBuffers,
|
||||||
|
totalSamples,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to generate silent audio:", error);
|
console.warn(`Failed to process audio source ${source.file.name}:`, error);
|
||||||
throw new Error("Unable to generate audio for empty timeline.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a complex filter to mix all audio sources
|
// clamp to prevent clipping
|
||||||
const inputFiles: string[] = [];
|
for (const channel of mixBuffers) {
|
||||||
const filterInputs: string[] = [];
|
for (let i = 0; i < channel.length; i++) {
|
||||||
|
channel[i] = Math.max(-1, Math.min(1, channel[i]));
|
||||||
try {
|
|
||||||
for (let i = 0; i < audioMixSources.length; i++) {
|
|
||||||
const element = audioMixSources[i];
|
|
||||||
const inputName = `input_${i}.${element.file.name.split(".").pop()}`;
|
|
||||||
inputFiles.push(inputName);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await ffmpeg.writeFile(
|
|
||||||
inputName,
|
|
||||||
new Uint8Array(await element.file.arrayBuffer()),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to write file ${element.file.name}:`, error);
|
|
||||||
throw new Error(
|
|
||||||
`Unable to process file: ${element.file.name}. The file may be corrupted or in an unsupported format.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualStart = element.trimStart;
|
|
||||||
const actualDuration = element.duration;
|
|
||||||
|
|
||||||
const filterName = `audio_${i}`;
|
|
||||||
filterInputs.push(
|
|
||||||
`[${i}:a]atrim=start=${actualStart}:duration=${actualDuration},asetpts=PTS-STARTPTS,adelay=${element.startTime * 1000}|${element.startTime * 1000}[${filterName}]`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mixFilter =
|
|
||||||
audioMixSources.length === 1
|
|
||||||
? `[audio_0]aresample=44100,aformat=sample_fmts=s16:channel_layouts=stereo[out]`
|
|
||||||
: `${filterInputs.map((_, i) => `[audio_${i}]`).join("")}amix=inputs=${audioMixSources.length}:duration=longest:dropout_transition=2,aresample=44100,aformat=sample_fmts=s16:channel_layouts=stereo[out]`;
|
|
||||||
|
|
||||||
const complexFilter = [...filterInputs, mixFilter].join(";");
|
|
||||||
const outputName = "timeline_audio.wav";
|
|
||||||
|
|
||||||
const ffmpegArgs = [
|
|
||||||
...inputFiles.flatMap((name) => ["-i", name]),
|
|
||||||
"-filter_complex",
|
|
||||||
complexFilter,
|
|
||||||
"-map",
|
|
||||||
"[out]",
|
|
||||||
"-t",
|
|
||||||
totalDuration.toString(),
|
|
||||||
"-c:a",
|
|
||||||
"pcm_s16le",
|
|
||||||
"-ar",
|
|
||||||
"44100",
|
|
||||||
outputName,
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
await ffmpeg.exec(ffmpegArgs);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("FFmpeg execution failed:", error);
|
|
||||||
throw new Error(
|
|
||||||
"Audio processing failed. Some audio files may be corrupted or incompatible.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await ffmpeg.readFile(outputName);
|
|
||||||
const blob = new Blob([data], { type: "audio/wav" });
|
|
||||||
|
|
||||||
return blob;
|
|
||||||
} catch (error) {
|
|
||||||
for (const inputFile of inputFiles) {
|
|
||||||
try {
|
|
||||||
await ffmpeg.deleteFile(inputFile);
|
|
||||||
} catch (cleanupError) {
|
|
||||||
console.warn(`Failed to cleanup file ${inputFile}:`, cleanupError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await ffmpeg.deleteFile("timeline_audio.wav");
|
|
||||||
} catch (cleanupError) {
|
|
||||||
console.warn("Failed to cleanup output file:", cleanupError);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
for (const inputFile of inputFiles) {
|
|
||||||
try {
|
|
||||||
await ffmpeg.deleteFile(inputFile);
|
|
||||||
} catch (cleanupError) {}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await ffmpeg.deleteFile("timeline_audio.wav");
|
|
||||||
} catch (cleanupError) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// interleave channels for wav output
|
||||||
|
const interleavedSamples = new Float32Array(totalSamples * NUM_CHANNELS);
|
||||||
|
for (let i = 0; i < totalSamples; i++) {
|
||||||
|
interleavedSamples[i * 2] = mixBuffers[0][i];
|
||||||
|
interleavedSamples[i * 2 + 1] = mixBuffers[1][i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createWavBlob({ samples: interleavedSamples });
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateSilentAudio = async (durationSeconds: number): Promise<Blob> => {
|
async function decodeAndMixAudioSource({
|
||||||
const ffmpeg = await initFFmpeg();
|
source,
|
||||||
const outputName = "silent.wav";
|
mixBuffers,
|
||||||
|
totalSamples,
|
||||||
|
}: {
|
||||||
|
source: { file: File; startTime: number; duration: number; trimStart: number };
|
||||||
|
mixBuffers: Float32Array[];
|
||||||
|
totalSamples: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
const input = new Input({
|
||||||
|
source: new BlobSource(source.file),
|
||||||
|
formats: ALL_FORMATS,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
const audioTrack = await input.getPrimaryAudioTrack();
|
||||||
await ffmpeg.exec([
|
if (!audioTrack) return;
|
||||||
"-f",
|
|
||||||
"lavfi",
|
|
||||||
"-i",
|
|
||||||
`anullsrc=channel_layout=stereo:sample_rate=44100`,
|
|
||||||
"-t",
|
|
||||||
durationSeconds.toString(),
|
|
||||||
"-c:a",
|
|
||||||
"pcm_s16le",
|
|
||||||
outputName,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const data = await ffmpeg.readFile(outputName);
|
const sink = new AudioBufferSink(audioTrack);
|
||||||
const blob = new Blob([data], { type: "audio/wav" });
|
const trimEnd = source.trimStart + source.duration;
|
||||||
|
|
||||||
return blob;
|
for await (const { buffer, timestamp } of sink.buffers(
|
||||||
} catch (error) {
|
source.trimStart,
|
||||||
console.error("Failed to generate silent audio:", error);
|
trimEnd,
|
||||||
throw error;
|
)) {
|
||||||
} finally {
|
const relativeTime = timestamp - source.trimStart;
|
||||||
try {
|
const outputStartSample = Math.floor(
|
||||||
await ffmpeg.deleteFile(outputName);
|
(source.startTime + relativeTime) * SAMPLE_RATE,
|
||||||
} catch (cleanupError) {
|
);
|
||||||
// Silent cleanup
|
|
||||||
|
// resample if needed
|
||||||
|
const resampleRatio = SAMPLE_RATE / buffer.sampleRate;
|
||||||
|
|
||||||
|
for (let ch = 0; ch < NUM_CHANNELS; ch++) {
|
||||||
|
const sourceChannel = Math.min(ch, buffer.numberOfChannels - 1);
|
||||||
|
const channelData = buffer.getChannelData(sourceChannel);
|
||||||
|
const outputChannel = mixBuffers[ch];
|
||||||
|
|
||||||
|
const resampledLength = Math.floor(channelData.length * resampleRatio);
|
||||||
|
for (let i = 0; i < resampledLength; i++) {
|
||||||
|
const outputIdx = outputStartSample + i;
|
||||||
|
if (outputIdx < 0 || outputIdx >= totalSamples) continue;
|
||||||
|
|
||||||
|
const sourceIdx = Math.floor(i / resampleRatio);
|
||||||
|
if (sourceIdx < channelData.length) {
|
||||||
|
outputChannel[outputIdx] += channelData[sourceIdx];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function createWavBlob({ samples }: { samples: Float32Array }): Blob {
|
||||||
|
const numChannels = NUM_CHANNELS;
|
||||||
|
const bitsPerSample = 16;
|
||||||
|
const bytesPerSample = bitsPerSample / 8;
|
||||||
|
const numSamples = samples.length / numChannels;
|
||||||
|
const dataSize = numSamples * numChannels * bytesPerSample;
|
||||||
|
const buffer = new ArrayBuffer(44 + dataSize);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
// riff header
|
||||||
|
writeString({ view, offset: 0, str: "RIFF" });
|
||||||
|
view.setUint32(4, 36 + dataSize, true);
|
||||||
|
writeString({ view, offset: 8, str: "WAVE" });
|
||||||
|
|
||||||
|
// fmt chunk
|
||||||
|
writeString({ view, offset: 12, str: "fmt " });
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
view.setUint16(20, 1, true);
|
||||||
|
view.setUint16(22, numChannels, true);
|
||||||
|
view.setUint32(24, SAMPLE_RATE, true);
|
||||||
|
view.setUint32(28, SAMPLE_RATE * numChannels * bytesPerSample, true);
|
||||||
|
view.setUint16(32, numChannels * bytesPerSample, true);
|
||||||
|
view.setUint16(34, bitsPerSample, true);
|
||||||
|
|
||||||
|
// data chunk
|
||||||
|
writeString({ view, offset: 36, str: "data" });
|
||||||
|
view.setUint32(40, dataSize, true);
|
||||||
|
|
||||||
|
// convert float32 to int16 and write
|
||||||
|
let offset = 44;
|
||||||
|
for (let i = 0; i < samples.length; i++) {
|
||||||
|
const sample = Math.max(-1, Math.min(1, samples[i]));
|
||||||
|
const int16 = sample < 0 ? sample * 0x8000 : sample * 0x7fff;
|
||||||
|
view.setInt16(offset, int16, true);
|
||||||
|
offset += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Blob([buffer], { type: "audio/wav" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeString({
|
||||||
|
view,
|
||||||
|
offset,
|
||||||
|
str,
|
||||||
|
}: {
|
||||||
|
view: DataView;
|
||||||
|
offset: number;
|
||||||
|
str: string;
|
||||||
|
}): void {
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
view.setUint8(offset + i, str.charCodeAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -44,7 +44,7 @@
|
|||||||
"feed": "^5.1.0",
|
"feed": "^5.1.0",
|
||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"mediabunny": "^1.9.3",
|
"mediabunny": "^1.29.1",
|
||||||
"motion": "^12.18.1",
|
"motion": "^12.18.1",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "16.1.3",
|
"next": "16.1.3",
|
||||||
@@ -834,7 +834,7 @@
|
|||||||
|
|
||||||
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||||
|
|
||||||
"mediabunny": ["mediabunny@1.9.3", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-uVspdzrpwvW8NxtGspHPOJvdNSY0dNFEmOQngSheX9PYX9oXijZENBjfYWOgm9c9IgSSlN2bdJF9iyPyY3f+fQ=="],
|
"mediabunny": ["mediabunny@1.29.1", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-RrlKs69MxRGa/l9cMGeI4hzuTSgchOGFHk9lIAuu9EcbSdJ05gDbRsTY67dojAiyUP3ic4PExij5OqDxqSv82Q=="],
|
||||||
|
|
||||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user