0.10.0: audio tracks — assets, per-scene lanes, editor + export playback
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b9f2ba9e47
commit
04f867547d
14 files changed
+2057
-42
No files matched your search
+99
-6
@@ -1,6 +1,6 @@
|
||||
import type { Scene } from '@/types/scene'
|
||||
|
||||
const RUNTIME_VERSION = '19'
|
||||
const RUNTIME_VERSION = '20'
|
||||
|
||||
const ENGINE_SOURCE = `(function(){
|
||||
if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return;
|
||||
@@ -153,6 +153,61 @@ const ENGINE_SOURCE = `(function(){
|
||||
bgVideo.style.cssText='position:absolute;inset:0;width:100%;height:100%;object-fit:cover;pointer-events:none;z-index:0;';
|
||||
container.appendChild(bgVideo);
|
||||
}
|
||||
// --- Audio pool ---
|
||||
var audioPool={};
|
||||
var audioAssetsById={};
|
||||
(scene.assets||[]).forEach(function(a){ if(a.kind==='audio') audioAssetsById[a.id]=a; });
|
||||
var audioClips=scene.audioClips||[];
|
||||
var audioTracksById={};
|
||||
(scene.audioTracks||[]).forEach(function(t){ audioTracksById[t.id]=t; });
|
||||
function disposeAudioClip(clipId){
|
||||
var entry=audioPool[clipId];
|
||||
if(!entry) return;
|
||||
entry.el.pause();
|
||||
entry.el.src='';
|
||||
if(entry.el.parentNode) entry.el.parentNode.removeChild(entry.el);
|
||||
delete audioPool[clipId];
|
||||
}
|
||||
function disposeAllAudio(){
|
||||
for(var k in audioPool){ if(Object.prototype.hasOwnProperty.call(audioPool,k)) disposeAudioClip(k); }
|
||||
}
|
||||
function syncAudio(t, isPlaying){
|
||||
for(var ci=0;ci<audioClips.length;ci++){
|
||||
var clip=audioClips[ci];
|
||||
var asset=audioAssetsById[clip.audioAssetId];
|
||||
if(!asset || !asset.src){ disposeAudioClip(clip.id); continue; }
|
||||
var entry=audioPool[clip.id];
|
||||
if(!entry || entry.assetId !== clip.audioAssetId){
|
||||
if(entry) disposeAudioClip(clip.id);
|
||||
var el=document.createElement('audio');
|
||||
el.preload='auto';
|
||||
el.src=asset.src;
|
||||
el.style.display='none';
|
||||
container.appendChild(el);
|
||||
entry={ el: el, assetId: clip.audioAssetId };
|
||||
audioPool[clip.id]=entry;
|
||||
}
|
||||
var track=audioTracksById[clip.audioTrackId];
|
||||
var trimS=clip.trimStart||0;
|
||||
var trimE=clip.trimEnd||0;
|
||||
var winLen=Math.max(0, asset.duration - trimS - trimE);
|
||||
var inWindow=t>=clip.startTime && t<(clip.startTime+winLen);
|
||||
var shouldPlay=isPlaying && inWindow && !(track && track.muted);
|
||||
if(shouldPlay){
|
||||
var audioTime=t - clip.startTime + trimS;
|
||||
if(entry.el.paused){
|
||||
try { entry.el.currentTime=audioTime; } catch(e) {}
|
||||
var p=entry.el.play();
|
||||
if(p && typeof p.catch==='function') p.catch(function(){});
|
||||
} else {
|
||||
var drift=Math.abs(entry.el.currentTime - audioTime);
|
||||
if(drift > 0.25){ try { entry.el.currentTime=audioTime; } catch(e) {} }
|
||||
}
|
||||
} else if(!entry.el.paused){
|
||||
entry.el.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
var canvas=document.createElement('canvas');
|
||||
canvas.style.cssText='display:block;width:100%;height:100%;position:relative;z-index:1;';
|
||||
container.appendChild(canvas);
|
||||
@@ -397,6 +452,7 @@ const ENGINE_SOURCE = `(function(){
|
||||
if(rangeLen<=0){
|
||||
draw(currentT);
|
||||
syncBgVideo(currentT);
|
||||
syncAudio(currentT, !paused);
|
||||
if(onFrame) onFrame(currentT, paused);
|
||||
rafId=requestAnimationFrame(frame);
|
||||
return;
|
||||
@@ -420,6 +476,7 @@ const ENGINE_SOURCE = `(function(){
|
||||
if(runtime.forcePlay){ paused=false; runtime.forcePlay=false; lastNow=0; }
|
||||
draw(currentT);
|
||||
syncBgVideo(currentT);
|
||||
syncAudio(currentT, !paused);
|
||||
if(onFrame) onFrame(currentT, paused);
|
||||
rafId=requestAnimationFrame(frame);
|
||||
}
|
||||
@@ -825,6 +882,7 @@ const ENGINE_SOURCE = `(function(){
|
||||
ro.disconnect();
|
||||
canvas.remove();
|
||||
if(bgVideo) bgVideo.remove();
|
||||
disposeAllAudio();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -908,16 +966,51 @@ export function mountPlayer(
|
||||
// ---- Export helpers ----
|
||||
|
||||
function jsonForExport(scene: Scene): string {
|
||||
// Data-URL background videos are stripped from the export — local video
|
||||
// files can't be embedded reasonably. The user needs to host the media
|
||||
// externally and set a URL in settings before exporting.
|
||||
// Data-URL background videos and audio assets are stripped from the
|
||||
// export — local media can't be embedded reasonably, and doing so
|
||||
// would bloat the bundle into uselessness. The user has to host the
|
||||
// file externally and point `backgroundVideo.src` / `AudioAsset.src`
|
||||
// at that URL before exporting.
|
||||
const warnings: string[] = []
|
||||
let clone: Scene = scene
|
||||
|
||||
const bg = scene.backgroundVideo
|
||||
if (bg && bg.src && bg.src.startsWith('data:')) {
|
||||
warnings.push('background video')
|
||||
const { backgroundVideo, ...rest } = scene
|
||||
void backgroundVideo
|
||||
return JSON.stringify(rest)
|
||||
clone = rest as Scene
|
||||
}
|
||||
return JSON.stringify(scene)
|
||||
|
||||
const audioAssets = clone.assets.filter(
|
||||
(a) => a.kind === 'audio' && a.src && a.src.startsWith('data:'),
|
||||
)
|
||||
if (audioAssets.length > 0) {
|
||||
warnings.push(
|
||||
`${audioAssets.length} audio asset(s) without external URL`,
|
||||
)
|
||||
// Replace the `src` with an empty string so the exported bundle's
|
||||
// lookups still find the asset record (duration, name) and the
|
||||
// runtime can render an empty block rather than crashing on a
|
||||
// missing asset.
|
||||
clone = {
|
||||
...clone,
|
||||
assets: clone.assets.map((a) =>
|
||||
a.kind === 'audio' && a.src && a.src.startsWith('data:')
|
||||
? { ...a, src: '' }
|
||||
: a,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length > 0 && typeof window !== 'undefined' && window.console) {
|
||||
window.console.warn(
|
||||
'[ruwio] export: stripped ' +
|
||||
warnings.join(', ') +
|
||||
' — host the files externally and set their src fields before exporting.',
|
||||
)
|
||||
}
|
||||
return JSON.stringify(clone)
|
||||
}
|
||||
|
||||
export interface ExportOptions {
|
||||
|
||||
Reference in new issue
Block a user