2023-09-24 18:39:52 +02:00
import {
makeEven ,
clone ,
} from './utils.js' ;
const FfmpegExporter = function ( ) {
let isFfmpegLoaded = false ;
let isFfmpegAttached = ( ) => {
return document . getElementById ( "ffmpeg.min.js" ) !== null ;
} ;
const attachFfmpeg = ( ) => {
if ( ! isFfmpegAttached ( ) ) {
// this does not work
// we refuse solving this, by simply attaching the script
// in the template from the beginning
//console.log("FFmpegExport::attachFfmpeg", "not attached yet, doing it");
var s = document . createElement ( "script" ) ;
s . id = "ffmpeg.min.js" ;
s . type = "application/javascript" ;
s . src = "/web/ffmpeg_modules/ffmpeg.min.js" ;
const mom = document . getElementById ( 'body' ) ;
mom . appendChild ( s ) ;
} else {
//console.log("FFmpegExport::attachFfmpeg", "already attached");
}
} ;
const createFfmpeg = ( ) => {
return new Promise ( ( resolve ) => {
if ( ! isFfmpegLoaded ) {
attachFfmpeg ( ) ;
const {
createFFmpeg
} = FFmpeg ;
this . ffmpeg = createFFmpeg ( {
log : false
} ) ;
window . ffmpeg = this . ffmpeg ;
this . ffmpeg . setLogger ( ( {
type ,
message
} ) => {
if ( typeof message === 'string' && message . toLowerCase ( ) . indexOf ( 'error' ) >= 0 ) {
if ( confirm ( 'Oh, there seems to be an error transcoding the video.\n'
+ 'Please either decrease resolution or render Frames instead of mp4.\n'
+ '\n'
+ 'Should we reload the page to restore from the error? Your project should still be there afterwards. If you\'re a bit paranoid (nobody blames you), then you can also first save your project to a zipfile and then reload the page yourself' ) ) {
window . location . reload ( ) ;
}
}
console . log ( "FFmpegExport::renderDiary" , type , message ) ;
//type can be one of following:
//info: internal workflow debug messages
//fferr: ffmpeg native stderr output
//ffout: ffmpeg native stdout output
} ) ;
let texts = [
"We perceive loading bars differently depending on what information they show us. This one for exampl" ,
"something I always wanted to tell you, is how beautiful you are. " ,
"The more you wait, the more it won't happen. Or maybe it will, I don't know. I'm a computer program." ,
"Waiting is the rust of the soul. " ,
"Waiting is a sin against both the time still to come and the moments one is currently disregarding. " ,
"Things may come to those who wait, but only the things left by those who hustle. " ,
"There is no great achievement that is not the result of patient working and waiting. " ,
"What we are waiting for is not as important as what happens while we are waiting. Trust the process." ,
"The worst part of life is waiting. The best part of life is to have someone worth waiting for. " ,
"You are not just waiting in vain. There is a purpose behind every delay. And if there is no purpose," ,
] ;
let text = texts [ Math . floor ( Math . random ( 0 , texts . length ) ) ] ;
ffmpeg . setProgress ( ( {
ratio
} ) => {
const percent = ratio * 100 ;
//let text = "somthing I always wanted to tell you, is how beautiful you are ";
//let text = "The more you wait, the more it won't happen. Or maybe it will, I don't know. I'm a computer program.";
let innerHTML = "|" ;
for ( let i = 0 ; i < 100 ; i ++ ) {
if ( i < percent ) {
innerHTML += text [ i % text . length ] ;
} else {
innerHTML += "-" ;
}
}
innerHTML += "|" ;
let progress = document . getElementById ( "export_progress" ) ;
progress . innerHTML = innerHTML ;
/ *
* ratio is a float number between 0 to 1.
* /
} ) ;
const loadFfmpeg = async ( ffmpeg ) => {
await ffmpeg . load ( ) ;
// mount ffmpeg in oF
if ( FS . readdir ( "/data" ) . indexOf ( "export" ) < 0 ) {
FS . mkdir ( "/data/export" ) ;
}
if ( FS . readdir ( "/data/export" ) . indexOf ( "frames" ) < 0 ) {
FS . mkdir ( "/data/export/frames" ) ;
}
ffmpeg . coreFS ( ) . mkdir ( "/frames" ) ;
FS . mount ( FS . filesystems . PROXYFS , {
root : "/frames" ,
fs : ffmpeg . coreFS ( )
} , "/data/export/frames" ) ;
isFfmpegLoaded = true ;
resolve ( ) ;
} ;
loadFfmpeg ( this . ffmpeg ) ;
} else { // already loaded
resolve ( ) ;
}
} ) ;
} ;
const transcodeVideo = async ( finishedCallback ) => {
const ffmpeg = this . ffmpeg ;
//const of_framesDir = '/data/export/frames';
const ffmpeg _framesDir = '/frames' ;
// const frameNames = FS.readdir(of_framesDir).splice(2); // remove '.', '..'
// ffmpeg.FS('mkdir', ffmpeg_framesDir);
// for (let i = 0; i < frameNames.length; i++) {
// const frameBuffer = FS.readFile(of_framesDir + "/" + frameNames[i]);
// ffmpeg.FS('writeFile', ffmpeg_framesDir + "/" + frameNames[i], frameBuffer);
// }
const progress _task = document . getElementById ( 'export_progress_task' ) ;
const progress = document . getElementById ( 'export_progress' ) ;
progress _task . innerHTML = 'transcoding video' ;
{
let innerHTML = "|" ;
for ( let i = 0 ; i < 100 ; i ++ ) {
innerHTML += "-" ;
}
innerHTML += "|" ;
progress . innerHTML = innerHTML ;
}
await ffmpeg . run ( '-framerate' , '30' , '-pattern_type' , 'glob' , '-i' , ` ${ ffmpeg _framesDir } /*.png ` , '-c:v' , 'libx264' , '-pix_fmt' , 'yuv420p' , 'output.mp4' ) ;
progress _task . innerHTML = 'preparing download' ;
progress . innerHTML = '|----------------------------------------------------------------------------------------------------|'
const data = ffmpeg . FS ( 'readFile' , 'output.mp4' ) ;
progress . innerHTML = '|::::::::::------------------------------------------------------------------------------------------|'
const buffy = URL . createObjectURL ( new Blob ( [ data . buffer ] , {
type : 'video/mp4'
} ) ) ;
progress . innerHTML = '|:::::::::::::::::::::::::---------------------------------------------------------------------------|'
// yeey, let's create a timestamp!
let date = new Date ( ) ;
const offset = date . getTimezoneOffset ( ) ;
date = new Date ( date . getTime ( ) - ( offset * 60 * 1000 ) ) ;
progress . innerHTML = '|:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::---------------------------------------|'
const timestamp = date . toISOString ( ) ;
// phew.. alright, it's not pretty but we did it
const filename = tp . sheet . project . address . projectId + "_" + timestamp + ".mp4" ;
// now: downloading!
let link = document . createElement ( "a" ) ;
link . href = buffy ;
link . download = filename ;
link . click ( ) ;
progress . innerHTML = '|::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::|'
setTimeout ( ( ) => {
progress _task . innerHTML = 'idle' ;
progress . innerHTML = '|----------------------------------------------------------------------------------------------------|'
finishedCallback ( ) ;
} , 500 ) ;
// no video
//const video = document.getElementById('player');
//video.src = URL.createObjectURL(new Blob([data.buffer], {
//type: 'video/mp4'
//}));
//video.style.display = 'flex';
} ;
// public
this . init = createFfmpeg ;
this . transcodeVideo = transcodeVideo ;
}
const Exporter = function ( ) {
const exporterDom = document . getElementById ( 'exporter' ) ;
const exporterDomChild = document . querySelector ( '.exporterChild' ) ;
const exporterOptionsDom = document . getElementById ( 'exporter_options' ) ;
let can _export _mp4 = true ;
const options = {
artboard : {
width : 0 ,
height : 0 ,
pixelDensity : 1 ,
userScale : 1 ,
} ,
} ;
const renderDimensions = {
width : 0 ,
height : 0 ,
timeScale : 1 ,
} ;
const updateArtboardOptions = ( ) => {
2024-01-09 12:30:55 +01:00
const artboardValues = clone ( Module . getArtboardProps ( ) ) ;
if ( typeof artboardValues . backgroundColor === 'object' ) {
artboardValues . color = artboardValues . backgroundColor ;
delete artboardValues . backgroundColor ;
}
options . artboard = { ... options . artboard , ... artboardValues } ;
2023-09-24 18:39:52 +02:00
//options.artboard.width = getArtboard().theatreObject.value.width;
//options.artboard.height = getArtboard().theatreObject.value.height;
options . artboard . pixelDensity = getArtboard ( ) . theatreObject . value . pixelDensity ;
[ ... exporterDom . querySelectorAll ( '.artboard_width' ) ] . forEach ( ( e ) => {
e . innerHTML = options . artboard . width ;
} ) ;
[ ... exporterDom . querySelectorAll ( '.artboard_height' ) ] . forEach ( ( e ) => {
e . innerHTML = options . artboard . height ;
} ) ;
[ ... exporterDom . querySelectorAll ( '.artboard_pixelDensity' ) ] . forEach ( ( e ) => {
e . innerHTML = options . artboard . pixelDensity ;
e . remove ( ) ; // NOTE: as we ignore pixel density for the export, it's not necessary to show it here
} ) ;
} ;
const setArtboardPropsToRenderDimensions = ( ) => {
const artboardValues = clone ( options . artboard ) ; //{...options.artboard, ...renderDimensions};
2024-01-09 12:30:55 +01:00
if ( typeof artboardValues . backgroundColor === 'object' ) {
artboardValues . color = artboardValues . backgroundColor ;
delete artboardValues . backgroundColor ;
}
2023-09-24 18:39:52 +02:00
const densityRatio = renderDimensions . width / options . artboard . width ;
//artboardValues.pixelDensity *= densityRatio;
artboardValues . pixelDensity = densityRatio ;
const artboardCppProps = getArtboard ( ) . values2cppProps ( artboardValues ) ;
const currentArtboardValues = Module . getArtboardProps ( ) ;
if ( currentArtboardValues . width !== artboardCppProps . width
|| currentArtboardValues . height !== artboardCppProps . height
|| currentArtboardValues . pixelDensity !== artboardCppProps . pixelDensity ) {
window . isRenderDirty = true ;
2024-01-09 12:30:55 +01:00
if ( typeof artboardCppProps . color === 'object' ) {
artboardCppProps . backgroundColor = artboardCppProps . color ;
delete artboardCppProps . color ;
}
2023-09-24 18:39:52 +02:00
Module . setArtboardProps ( artboardCppProps ) ;
}
Module . setTimeScale ( renderDimensions . timeScale ) ;
} ;
const resetArtboardProps = ( ) => {
//const artboardCppProps = getArtboard().values2cppProps(options.artboard);
const artboardValues = getArtboard ( ) . theatreObject . value ;
const artboardCppProps = getArtboard ( ) . values2cppProps ( artboardValues ) ;
Module . setArtboardProps ( artboardCppProps ) ;
Module . setTimeScale ( 1.0 ) ;
} ;
const updateRenderDimensions = ( ) => {
const currentDimensions = {
width : options . artboard . width * options . artboard . pixelDensity ,
height : options . artboard . height * options . artboard . pixelDensity ,
} ;
const artboardUserScaleLabelDom = document . getElementById ( 'artboard_scale_label' ) ;
artboardUserScaleLabelDom . innerHTML = options . artboard . userScale ;
const timeScaleLabelDom = document . getElementById ( 'render_timescale_label' ) ;
timeScaleLabelDom . innerHTML = renderDimensions . timeScale ;
const timelineLength _seconds = window . tp . core . val ( window . tp . sheet . sequence . pointer . length ) ;
renderDimensions . width = makeEven ( currentDimensions . width * options . artboard . userScale * ( 1.0 / options . artboard . pixelDensity ) ) ;
renderDimensions . height = makeEven ( currentDimensions . height * options . artboard . userScale * ( 1.0 / options . artboard . pixelDensity ) ) ;
[ ... exporterDom . querySelectorAll ( '.render_width' ) ] . forEach ( ( e ) => {
e . innerHTML = renderDimensions . width ;
} ) ;
[ ... exporterDom . querySelectorAll ( '.render_height' ) ] . forEach ( ( e ) => {
e . innerHTML = renderDimensions . height ;
} ) ;
[ ... exporterDom . querySelectorAll ( '.render_pixels' ) ] . forEach ( ( e ) => {
// 12345678 => 12.345.678
function addDots ( nStr ) {
nStr += '' ;
let x = nStr . split ( '.' ) ;
let x1 = x [ 0 ] ;
let x2 = x . length > 1 ? '.' + x [ 1 ] : '' ;
var rgx = /(\d+)(\d{3})/ ;
while ( rgx . test ( x1 ) ) {
x1 = x1 . replace ( rgx , '$1' + '.' + '$2' ) ; // changed comma to dot here
}
return x1 + x2 ;
}
e . innerHTML = addDots ( ` ${ renderDimensions . width * renderDimensions . height } ` ) ;
} ) ;
[ ... exporterDom . querySelectorAll ( '.render_length' ) ] . forEach ( ( e ) => {
e . innerHTML = ( timelineLength _seconds / renderDimensions . timeScale ) . toFixed ( 2 ) ;
} ) ;
if ( renderDimensions . width * renderDimensions . height > 1920 * 1080 ) {
exporterDom . querySelector ( '.exporter_dimension_warning' ) . style . display = 'flex' ;
exporterDom . querySelector ( '#exporter_button_mp4' ) . disabled = true ;
can _export _mp4 = false ;
} else {
exporterDom . querySelector ( '.exporter_dimension_warning' ) . style . display = 'none' ;
exporterDom . querySelector ( '#exporter_button_mp4' ) . disabled = false ;
can _export _mp4 = true ;
}
} ;
const registerEvents = ( ) => {
const close _button = document . getElementById ( 'exporter_close' ) ;
close _button . addEventListener ( "click" , this . close ) ;
const open _button = document . getElementById ( 'exporter_open' ) ;
open _button . addEventListener ( "click" , this . open ) ;
const artboardUserScale _input = document . getElementById ( 'artboard_scale' ) ;
artboardUserScale _input . addEventListener ( 'change' , ( event ) => {
options . artboard . userScale = event . target . value ;
updateRenderDimensions ( ) ;
} ) ;
artboardUserScale _input . addEventListener ( 'input' , ( event ) => {
options . artboard . userScale = event . target . value ;
updateRenderDimensions ( ) ;
} ) ;
const timeScale _input = document . getElementById ( 'render_timescale' ) ;
timeScale _input . addEventListener ( 'change' , ( event ) => {
renderDimensions . timeScale = event . target . value ;
updateRenderDimensions ( ) ;
} ) ;
timeScale _input . addEventListener ( 'input' , ( event ) => {
renderDimensions . timeScale = event . target . value ;
updateRenderDimensions ( ) ;
} ) ;
} ;
window . isRenderDirty = true ;
const cancel = ( e ) => {
e . stopPropagation ( ) ;
if ( confirm ( "Closing the export window during an active export will cancel the rendering process and reload the page. Is that okay for you?" ) ) {
window . location . reload ( ) ;
}
} ;
const resetAfter = ( ) => {
exporterDom . querySelector ( '#exporter_button_mp4' ) . disabled = ! can _export _mp4 ;
exporterDom . querySelector ( '#exporter_button_zip' ) . disabled = false ;
const close _button = document . getElementById ( 'exporter_close' ) ;
close _button . removeEventListener ( "click" , cancel ) ;
close _button . addEventListener ( "click" , this . close ) ;
exporterDom . querySelector ( '#exporter_render_info' ) . style . display = 'none' ;
const progress _task = document . getElementById ( 'export_progress_task' ) ;
const progress = document . getElementById ( 'export_progress' ) ;
progress _task . innerHTML = 'idle' ;
progress . innerHTML = '|----------------------------------------------------------------------------------------------------|'
} ;
this . renderFrames = ( exportType ) => {
exporterDom . querySelector ( '#exporter_button_mp4' ) . disabled = true ;
exporterDom . querySelector ( '#exporter_button_zip' ) . disabled = true ;
const close _button = document . getElementById ( 'exporter_close' ) ;
close _button . addEventListener ( "click" , cancel ) ;
close _button . removeEventListener ( "click" , this . close ) ;
exporterDom . querySelector ( '#exporter_render_info' ) . style . display = 'flex' ;
setArtboardPropsToRenderDimensions ( ) ;
if ( window . isRenderDirty ) {
tp . sheet . sequence . pause ( ) ;
if ( exportType === 'zip' ) {
window . renderDone = ( ) => {
window . isRenderDirty = false ;
const projectName = tp . sheet . project . address . projectId ;
Module . exportFramesAsZip ( projectName ) ;
// progress is being set in separate cpp thread
// so we reset some things in ofApp.h -> ZipSaver
resetAfter ( ) ;
} ;
} else if ( exportType === 'mp4' ) {
window . renderDone = ( ) => {
window . isRenderDirty = false ;
this . ffmpegExporter
. init ( )
. then ( ( ) => {
this . ffmpegExporter
. transcodeVideo ( resetAfter ) ;
} ) ;
} ;
} else {
window . renderDone = ( ) => {
console . log ( 'rendering done! now.. what?' ) ;
window . isRenderDirty = false ;
} ;
}
Module . setRendering ( true ) ;
} else {
if ( exportType === 'zip' ) {
const projectName = tp . sheet . project . address . projectId ;
Module . exportFramesAsZip ( projectName ) ;
// progress is being set in separate cpp thread
// so we reset some things in ofApp.h -> ZipSaver
resetAfter ( ) ;
} else if ( exportType === 'mp4' ) {
this . ffmpegExporter
. init ( )
. then ( ( ) => {
this . ffmpegExporter
. transcodeVideo ( resetAfter ) ;
} ) ;
} else {
console . log ( 'now.. what?' ) ;
}
}
} ;
let isInitialized = false ;
this . init = ( ) => {
return new Promise ( ( resolve ) => {
if ( ! isInitialized ) {
registerEvents ( ) ;
this . ffmpegExporter
. init ( )
. then ( ( ) => {
resolve ( ) ;
} ) ;
} else {
resolve ( ) ;
}
} ) ;
} ;
this . ffmpegExporter = new FfmpegExporter ( ) ;
this . open = ( ) => {
updateArtboardOptions ( ) ;
updateRenderDimensions ( ) ;
const renderWidthDom = exporterDom . querySelector ( '.render_width' ) ;
const renderHeightDom = exporterDom . querySelector ( '.render_height' ) ;
// exporterDom.style.display = 'flex';
exporterDom . classList . add ( 'exporterShown' ) ;
// exporterDomChild.style.marginBottom = '0vh';
} ;
this . close = ( ) => {
// exporterDom.style.display = 'none';
exporterDom . classList . remove ( 'exporterShown' ) ;
// exporterDomChild.style.marginBottom = '-50vh';
resetArtboardProps ( ) ;
} ;
// action
//init();
} ;
export {
Exporter
}