Menubar.Render.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843
  1. import * as THREE from 'three';
  2. import { UIPanel, UIRow, UIButton, UIInteger, UISelect, UIText } from './libs/ui.js';
  3. import { ViewportPathtracer } from './Viewport.Pathtracer.js';
  4. import { APP } from './libs/app.js';
  5. function MenubarRender( editor ) {
  6. const strings = editor.strings;
  7. const container = new UIPanel();
  8. container.setClass( 'menu' );
  9. const title = new UIPanel();
  10. title.setClass( 'title' );
  11. title.setTextContent( strings.getKey( 'menubar/render' ) );
  12. container.add( title );
  13. const options = new UIPanel();
  14. options.setClass( 'options' );
  15. container.add( options );
  16. // Image
  17. let option = new UIRow();
  18. option.setClass( 'option' );
  19. option.setTextContent( strings.getKey( 'menubar/render/image' ) );
  20. option.onClick( function () {
  21. showImageDialog();
  22. } );
  23. options.add( option );
  24. // Video
  25. if ( 'VideoEncoder' in window ) {
  26. option = new UIRow();
  27. option.setClass( 'option' );
  28. option.setTextContent( strings.getKey( 'menubar/render/video' ) );
  29. option.onClick( function () {
  30. showVideoDialog();
  31. } );
  32. options.add( option );
  33. }
  34. // Image Dialog
  35. function showImageDialog() {
  36. const dialog = new RenderImageDialog( editor, strings );
  37. document.body.appendChild( dialog.dom );
  38. }
  39. // Video Dialog
  40. function showVideoDialog() {
  41. const dialog = new RenderVideoDialog( editor, strings );
  42. document.body.appendChild( dialog.dom );
  43. }
  44. return container;
  45. }
  46. class RenderImageDialog {
  47. constructor( editor, strings ) {
  48. const dom = document.createElement( 'div' );
  49. dom.className = 'Dialog';
  50. this.dom = dom;
  51. const background = document.createElement( 'div' );
  52. background.className = 'Dialog-background';
  53. background.addEventListener( 'click', () => this.close() );
  54. dom.appendChild( background );
  55. const content = document.createElement( 'div' );
  56. content.className = 'Dialog-content';
  57. dom.appendChild( content );
  58. // Title
  59. const titleBar = document.createElement( 'div' );
  60. titleBar.className = 'Dialog-title';
  61. titleBar.textContent = strings.getKey( 'menubar/render' ) + ' ' + strings.getKey( 'menubar/render/image' );
  62. content.appendChild( titleBar );
  63. // Body
  64. const body = document.createElement( 'div' );
  65. body.className = 'Dialog-body';
  66. content.appendChild( body );
  67. // Shading
  68. const shadingRow = new UIRow();
  69. body.appendChild( shadingRow.dom );
  70. shadingRow.add( new UIText( strings.getKey( 'sidebar/project/shading' ) ).setClass( 'Label' ) );
  71. const shadingTypeSelect = new UISelect().setOptions( {
  72. 'solid': 'SOLID',
  73. 'realistic': 'REALISTIC'
  74. } ).setWidth( '170px' ).onChange( refreshShadingRow ).setValue( 'solid' );
  75. shadingRow.add( shadingTypeSelect );
  76. const pathTracerMinSamples = 3;
  77. const pathTracerMaxSamples = 65536;
  78. const samplesNumber = new UIInteger( 16 ).setRange( pathTracerMinSamples, pathTracerMaxSamples );
  79. const samplesRow = new UIRow();
  80. samplesRow.add( new UIText( strings.getKey( 'sidebar/project/image/samples' ) ).setClass( 'Label' ) );
  81. samplesRow.add( samplesNumber );
  82. body.appendChild( samplesRow.dom );
  83. function refreshShadingRow() {
  84. samplesRow.setHidden( shadingTypeSelect.getValue() !== 'realistic' );
  85. }
  86. refreshShadingRow();
  87. // Resolution
  88. const resolutionRow = new UIRow();
  89. body.appendChild( resolutionRow.dom );
  90. resolutionRow.add( new UIText( strings.getKey( 'sidebar/project/resolution' ) ).setClass( 'Label' ) );
  91. const imageWidth = new UIInteger( 1024 ).setTextAlign( 'center' ).setWidth( '28px' );
  92. resolutionRow.add( imageWidth );
  93. resolutionRow.add( new UIText( '\u00D7' ).setTextAlign( 'center' ).setFontSize( '12px' ).setWidth( '12px' ) );
  94. const imageHeight = new UIInteger( 1024 ).setTextAlign( 'center' ).setWidth( '28px' );
  95. resolutionRow.add( imageHeight );
  96. // Buttons
  97. const buttonsRow = document.createElement( 'div' );
  98. buttonsRow.className = 'Dialog-buttons';
  99. body.appendChild( buttonsRow );
  100. const renderButton = new UIButton( strings.getKey( 'sidebar/project/render' ) );
  101. renderButton.setWidth( '80px' );
  102. renderButton.onClick( async () => {
  103. if ( shadingTypeSelect.getValue() === 'realistic' ) {
  104. let isMaterialsValid = true;
  105. editor.scene.traverseVisible( ( object ) => {
  106. if ( object.isMesh ) {
  107. const materials = Array.isArray( object.material ) ? object.material : [ object.material ];
  108. for ( let i = 0; i < materials.length; i ++ ) {
  109. const material = materials[ i ];
  110. if ( ! material.isMeshStandardMaterial ) {
  111. isMaterialsValid = false;
  112. return;
  113. }
  114. }
  115. }
  116. } );
  117. if ( isMaterialsValid === false ) {
  118. alert( strings.getKey( 'prompt/rendering/realistic/unsupportedMaterial' ) );
  119. return;
  120. }
  121. }
  122. //
  123. const json = editor.toJSON();
  124. const project = json.project;
  125. //
  126. const loader = new THREE.ObjectLoader();
  127. const camera = await loader.parseAsync( json.camera );
  128. camera.aspect = imageWidth.getValue() / imageHeight.getValue();
  129. camera.updateProjectionMatrix();
  130. camera.updateMatrixWorld();
  131. const scene = await loader.parseAsync( json.scene );
  132. const renderer = new THREE.WebGLRenderer( { antialias: true, logarithmicDepthBuffer: true } );
  133. renderer.setSize( imageWidth.getValue(), imageHeight.getValue() );
  134. renderer.setClearColor( editor.viewportColor );
  135. if ( project.shadows !== undefined ) renderer.shadowMap.enabled = project.shadows;
  136. if ( project.shadowType !== undefined ) renderer.shadowMap.type = project.shadowType;
  137. if ( project.toneMapping !== undefined ) renderer.toneMapping = project.toneMapping;
  138. if ( project.toneMappingExposure !== undefined ) renderer.toneMappingExposure = project.toneMappingExposure;
  139. // popup
  140. const width = imageWidth.getValue() / window.devicePixelRatio;
  141. const height = imageHeight.getValue() / window.devicePixelRatio;
  142. const left = ( screen.width - width ) / 2;
  143. const top = ( screen.height - height ) / 2;
  144. const output = window.open( '', '_blank', `location=no,left=${left},top=${top},width=${width},height=${height}` );
  145. const meta = document.createElement( 'meta' );
  146. meta.name = 'viewport';
  147. meta.content = 'width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0';
  148. output.document.head.appendChild( meta );
  149. output.document.body.style.background = '#000';
  150. output.document.body.style.margin = '0px';
  151. output.document.body.style.overflow = 'hidden';
  152. const canvas = renderer.domElement;
  153. canvas.style.width = width + 'px';
  154. canvas.style.height = height + 'px';
  155. output.document.body.appendChild( canvas );
  156. //
  157. switch ( shadingTypeSelect.getValue() ) {
  158. case 'solid':
  159. renderer.render( scene, camera );
  160. renderer.dispose();
  161. break;
  162. case 'realistic':
  163. const status = document.createElement( 'div' );
  164. status.style.position = 'absolute';
  165. status.style.top = '10px';
  166. status.style.left = '10px';
  167. status.style.color = 'white';
  168. status.style.fontFamily = 'system-ui';
  169. status.style.fontSize = '12px';
  170. output.document.body.appendChild( status );
  171. const pathTracer = new ViewportPathtracer( renderer );
  172. pathTracer.init( scene, camera );
  173. pathTracer.setSize( imageWidth.getValue(), imageHeight.getValue() );
  174. const maxSamples = Math.max( pathTracerMinSamples, Math.min( pathTracerMaxSamples, samplesNumber.getValue() ) );
  175. function animate() {
  176. if ( output.closed === true ) return;
  177. const samples = Math.floor( pathTracer.getSamples() ) + 1;
  178. if ( samples < maxSamples ) {
  179. requestAnimationFrame( animate );
  180. }
  181. pathTracer.update();
  182. const progress = Math.floor( samples / maxSamples * 100 );
  183. status.textContent = `${ samples } / ${ maxSamples } ( ${ progress }% )`;
  184. if ( progress === 100 ) {
  185. status.textContent += ' \u2713';
  186. }
  187. }
  188. animate();
  189. break;
  190. }
  191. this.close();
  192. } );
  193. buttonsRow.appendChild( renderButton.dom );
  194. const cancelButton = new UIButton( strings.getKey( 'menubar/render/cancel' ) );
  195. cancelButton.setWidth( '80px' );
  196. cancelButton.setMarginLeft( '8px' );
  197. cancelButton.onClick( () => this.close() );
  198. buttonsRow.appendChild( cancelButton.dom );
  199. }
  200. close() {
  201. this.dom.remove();
  202. }
  203. }
  204. class RenderVideoDialog {
  205. constructor( editor, strings ) {
  206. const dom = document.createElement( 'div' );
  207. dom.className = 'Dialog';
  208. this.dom = dom;
  209. const background = document.createElement( 'div' );
  210. background.className = 'Dialog-background';
  211. background.addEventListener( 'click', () => this.close() );
  212. dom.appendChild( background );
  213. const content = document.createElement( 'div' );
  214. content.className = 'Dialog-content';
  215. dom.appendChild( content );
  216. // Title
  217. const titleBar = document.createElement( 'div' );
  218. titleBar.className = 'Dialog-title';
  219. titleBar.textContent = strings.getKey( 'menubar/render' ) + ' ' + strings.getKey( 'menubar/render/video' );
  220. content.appendChild( titleBar );
  221. // Body
  222. const body = document.createElement( 'div' );
  223. body.className = 'Dialog-body';
  224. content.appendChild( body );
  225. // Resolution
  226. function toDiv2() {
  227. this.setValue( 2 * Math.floor( this.getValue() / 2 ) );
  228. }
  229. const resolutionRow = new UIRow();
  230. body.appendChild( resolutionRow.dom );
  231. resolutionRow.add( new UIText( strings.getKey( 'sidebar/project/resolution' ) ).setClass( 'Label' ) );
  232. const videoWidth = new UIInteger( 1024 ).setTextAlign( 'center' ).setWidth( '28px' ).setStep( 2 ).onChange( toDiv2 );
  233. resolutionRow.add( videoWidth );
  234. resolutionRow.add( new UIText( '\u00D7' ).setTextAlign( 'center' ).setFontSize( '12px' ).setWidth( '12px' ) );
  235. const videoHeight = new UIInteger( 1024 ).setTextAlign( 'center' ).setWidth( '28px' ).setStep( 2 ).onChange( toDiv2 );
  236. resolutionRow.add( videoHeight );
  237. const videoFPS = new UIInteger( 30 ).setTextAlign( 'center' ).setWidth( '20px' );
  238. resolutionRow.add( videoFPS );
  239. resolutionRow.add( new UIText( 'fps' ).setFontSize( '12px' ) );
  240. // Duration
  241. const videoDurationRow = new UIRow();
  242. videoDurationRow.add( new UIText( strings.getKey( 'sidebar/project/duration' ) ).setClass( 'Label' ) );
  243. body.appendChild( videoDurationRow.dom );
  244. const videoDuration = new UIInteger( 10 );
  245. videoDurationRow.add( videoDuration );
  246. // Quality
  247. const qualityRow = new UIRow();
  248. qualityRow.add( new UIText( strings.getKey( 'menubar/render/quality' ) ).setClass( 'Label' ) );
  249. body.appendChild( qualityRow.dom );
  250. const videoQuality = new UISelect().setOptions( {
  251. 'low': 'Low',
  252. 'medium': 'Medium',
  253. 'high': 'High',
  254. 'ultra': 'Ultra'
  255. } ).setWidth( '170px' ).setValue( 'high' );
  256. qualityRow.add( videoQuality );
  257. // Buttons
  258. const buttonsRow = document.createElement( 'div' );
  259. buttonsRow.className = 'Dialog-buttons';
  260. body.appendChild( buttonsRow );
  261. const renderButton = new UIButton( strings.getKey( 'sidebar/project/render' ) );
  262. renderButton.setWidth( '80px' );
  263. renderButton.onClick( async () => {
  264. const player = new APP.Player();
  265. await player.load( editor.toJSON() );
  266. player.setPixelRatio( 1 );
  267. player.setSize( videoWidth.getValue(), videoHeight.getValue() );
  268. player.setClearColor( editor.viewportColor );
  269. //
  270. const width = videoWidth.getValue() / window.devicePixelRatio;
  271. const height = videoHeight.getValue() / window.devicePixelRatio;
  272. const canvas = player.canvas;
  273. canvas.style.width = width + 'px';
  274. canvas.style.height = height + 'px';
  275. const left = ( screen.width - width ) / 2;
  276. const top = ( screen.height - height ) / 2;
  277. const output = window.open( '', '_blank', `location=no,left=${left},top=${top},width=${width},height=${height}` );
  278. const meta = document.createElement( 'meta' );
  279. meta.name = 'viewport';
  280. meta.content = 'width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0';
  281. output.document.head.appendChild( meta );
  282. output.document.body.style.background = '#000';
  283. output.document.body.style.margin = '0px';
  284. output.document.body.style.overflow = 'hidden';
  285. output.document.body.appendChild( canvas );
  286. const status = document.createElement( 'div' );
  287. status.style.position = 'absolute';
  288. status.style.top = '10px';
  289. status.style.left = '10px';
  290. status.style.color = 'white';
  291. status.style.fontFamily = 'system-ui';
  292. status.style.fontSize = '12px';
  293. status.style.textShadow = '0 0 2px black';
  294. output.document.body.appendChild( status );
  295. const video = document.createElement( 'video' );
  296. video.width = width;
  297. video.height = height;
  298. video.controls = true;
  299. video.loop = true;
  300. video.hidden = true;
  301. output.document.body.appendChild( video );
  302. output.addEventListener( 'unload', function () {
  303. if ( video.src.startsWith( 'blob:' ) ) {
  304. URL.revokeObjectURL( video.src );
  305. }
  306. } );
  307. //
  308. const fps = videoFPS.getValue();
  309. const duration = videoDuration.getValue();
  310. const frames = duration * fps;
  311. const encodedChunks = [];
  312. let codecConfig = null;
  313. const videoEncoder = new VideoEncoder( {
  314. output: ( chunk, metadata ) => {
  315. if ( metadata?.decoderConfig?.description ) {
  316. codecConfig = new Uint8Array( metadata.decoderConfig.description );
  317. }
  318. const chunkData = new Uint8Array( chunk.byteLength );
  319. chunk.copyTo( chunkData );
  320. encodedChunks.push( { data: chunkData, timestamp: chunk.timestamp, type: chunk.type } );
  321. },
  322. error: ( e ) => console.error( 'VideoEncoder error:', e )
  323. } );
  324. const qualityToBitrate = {
  325. 'low': 2e6,
  326. 'medium': 5e6,
  327. 'high': 10e6,
  328. 'ultra': 20e6
  329. };
  330. videoEncoder.configure( {
  331. codec: 'avc1.640028',
  332. width: videoWidth.getValue(),
  333. height: videoHeight.getValue(),
  334. bitrate: qualityToBitrate[ videoQuality.getValue() ],
  335. framerate: fps,
  336. avc: { format: 'avc' }
  337. } );
  338. let currentTime = 0;
  339. let aborted = false;
  340. for ( let i = 0; i < frames; i ++ ) {
  341. if ( output.closed ) {
  342. aborted = true;
  343. break;
  344. }
  345. player.render( currentTime );
  346. const bitmap = await createImageBitmap( canvas );
  347. const frame = new VideoFrame( bitmap, { timestamp: i * ( 1e6 / fps ) } );
  348. videoEncoder.encode( frame, { keyFrame: i % fps === 0 } );
  349. frame.close();
  350. bitmap.close();
  351. currentTime += 1 / fps;
  352. const progress = Math.floor( ( i + 1 ) / frames * 100 );
  353. status.textContent = `${ i + 1 } / ${ frames } ( ${ progress }% )`;
  354. }
  355. if ( ! aborted ) {
  356. await videoEncoder.flush();
  357. videoEncoder.close();
  358. output.document.body.removeChild( canvas );
  359. const mp4Data = createMP4( encodedChunks, codecConfig, videoWidth.getValue(), videoHeight.getValue(), fps );
  360. status.textContent = `${ frames } / ${ frames } ( 100% ) ${ formatFileSize( mp4Data.byteLength ) } \u2713`;
  361. video.src = URL.createObjectURL( new Blob( [ mp4Data ], { type: 'video/mp4' } ) );
  362. video.hidden = false;
  363. }
  364. player.dispose();
  365. this.close();
  366. } );
  367. buttonsRow.appendChild( renderButton.dom );
  368. const cancelButton = new UIButton( strings.getKey( 'menubar/render/cancel' ) );
  369. cancelButton.setWidth( '80px' );
  370. cancelButton.setMarginLeft( '8px' );
  371. cancelButton.onClick( () => this.close() );
  372. buttonsRow.appendChild( cancelButton.dom );
  373. }
  374. close() {
  375. this.dom.remove();
  376. }
  377. }
  378. // Simple MP4 muxer for H.264 encoded chunks
  379. function createMP4( chunks, avcC, width, height, fps ) {
  380. const timescale = 90000;
  381. const frameDuration = timescale / fps;
  382. function u32( value ) {
  383. return new Uint8Array( [ ( value >> 24 ) & 0xFF, ( value >> 16 ) & 0xFF, ( value >> 8 ) & 0xFF, value & 0xFF ] );
  384. }
  385. function u16( value ) {
  386. return new Uint8Array( [ ( value >> 8 ) & 0xFF, value & 0xFF ] );
  387. }
  388. function str( s ) {
  389. return new TextEncoder().encode( s );
  390. }
  391. function concat( ...arrays ) {
  392. const totalLength = arrays.reduce( ( sum, arr ) => sum + arr.length, 0 );
  393. const result = new Uint8Array( totalLength );
  394. let offset = 0;
  395. for ( const arr of arrays ) {
  396. result.set( arr, offset );
  397. offset += arr.length;
  398. }
  399. return result;
  400. }
  401. function box( type, ...contents ) {
  402. const data = concat( ...contents );
  403. const size = data.length + 8;
  404. return concat( u32( size ), str( type ), data );
  405. }
  406. function fullBox( type, version, flags, ...contents ) {
  407. return box( type, new Uint8Array( [ version, ( flags >> 16 ) & 0xFF, ( flags >> 8 ) & 0xFF, flags & 0xFF ] ), ...contents );
  408. }
  409. // ftyp
  410. const ftyp = box( 'ftyp',
  411. str( 'isom' ),
  412. u32( 512 ),
  413. str( 'isom' ), str( 'iso2' ), str( 'avc1' ), str( 'mp41' )
  414. );
  415. // Collect sample info
  416. const sampleSizes = [];
  417. const syncSamples = [];
  418. for ( let i = 0; i < chunks.length; i ++ ) {
  419. sampleSizes.push( chunks[ i ].data.length );
  420. if ( chunks[ i ].type === 'key' ) syncSamples.push( i + 1 );
  421. }
  422. // mdat
  423. let mdatSize = 8;
  424. for ( const chunk of chunks ) mdatSize += chunk.data.length;
  425. // stsd - Sample Description
  426. const avc1 = box( 'avc1',
  427. new Uint8Array( 6 ), // reserved
  428. u16( 1 ), // data reference index
  429. new Uint8Array( 16 ), // pre-defined + reserved
  430. u16( width ),
  431. u16( height ),
  432. u32( 0x00480000 ), // horizontal resolution 72 dpi
  433. u32( 0x00480000 ), // vertical resolution 72 dpi
  434. u32( 0 ), // reserved
  435. u16( 1 ), // frame count
  436. new Uint8Array( 32 ), // compressor name
  437. u16( 0x0018 ), // depth
  438. new Uint8Array( [ 0xFF, 0xFF ] ), // pre-defined
  439. box( 'avcC', avcC )
  440. );
  441. const stsd = fullBox( 'stsd', 0, 0, u32( 1 ), avc1 );
  442. // stts - Time-to-Sample
  443. const stts = fullBox( 'stts', 0, 0,
  444. u32( 1 ),
  445. u32( chunks.length ),
  446. u32( frameDuration )
  447. );
  448. // stsc - Sample-to-Chunk
  449. const stsc = fullBox( 'stsc', 0, 0,
  450. u32( 1 ),
  451. u32( 1 ), u32( chunks.length ), u32( 1 )
  452. );
  453. // stsz - Sample Sizes
  454. const stszData = [ u32( 0 ), u32( chunks.length ) ];
  455. for ( const size of sampleSizes ) stszData.push( u32( size ) );
  456. const stsz = fullBox( 'stsz', 0, 0, ...stszData );
  457. // stco - Chunk Offsets (placeholder, will be updated)
  458. const stco = fullBox( 'stco', 0, 0, u32( 1 ), u32( 0 ) );
  459. // stss - Sync Samples
  460. const stssData = [ u32( syncSamples.length ) ];
  461. for ( const sync of syncSamples ) stssData.push( u32( sync ) );
  462. const stss = fullBox( 'stss', 0, 0, ...stssData );
  463. // stbl
  464. const stbl = box( 'stbl', stsd, stts, stsc, stsz, stco, stss );
  465. // dinf
  466. const dref = fullBox( 'dref', 0, 0,
  467. u32( 1 ),
  468. fullBox( 'url ', 0, 1 )
  469. );
  470. const dinf = box( 'dinf', dref );
  471. // vmhd
  472. const vmhd = fullBox( 'vmhd', 0, 1, new Uint8Array( 8 ) );
  473. // minf
  474. const minf = box( 'minf', vmhd, dinf, stbl );
  475. // hdlr
  476. const hdlr = fullBox( 'hdlr', 0, 0,
  477. u32( 0 ), // pre-defined
  478. str( 'vide' ),
  479. new Uint8Array( 12 ), // reserved
  480. str( 'VideoHandler' ), new Uint8Array( 1 )
  481. );
  482. // mdhd
  483. const durationInTimescale = chunks.length * frameDuration;
  484. const mdhd = fullBox( 'mdhd', 0, 0,
  485. u32( 0 ), // creation time
  486. u32( 0 ), // modification time
  487. u32( timescale ),
  488. u32( durationInTimescale ),
  489. u16( 0x55C4 ), // language (und)
  490. u16( 0 ) // quality
  491. );
  492. // mdia
  493. const mdia = box( 'mdia', mdhd, hdlr, minf );
  494. // tkhd
  495. const tkhd = fullBox( 'tkhd', 0, 3,
  496. u32( 0 ), // creation time
  497. u32( 0 ), // modification time
  498. u32( 1 ), // track id
  499. u32( 0 ), // reserved
  500. u32( durationInTimescale ),
  501. new Uint8Array( 8 ), // reserved
  502. u16( 0 ), // layer
  503. u16( 0 ), // alternate group
  504. u16( 0 ), // volume
  505. u16( 0 ), // reserved
  506. // matrix
  507. u32( 0x00010000 ), u32( 0 ), u32( 0 ),
  508. u32( 0 ), u32( 0x00010000 ), u32( 0 ),
  509. u32( 0 ), u32( 0 ), u32( 0x40000000 ),
  510. u32( width << 16 ), // width (16.16 fixed point)
  511. u32( height << 16 ) // height (16.16 fixed point)
  512. );
  513. // trak
  514. const trak = box( 'trak', tkhd, mdia );
  515. // mvhd
  516. const mvhd = fullBox( 'mvhd', 0, 0,
  517. u32( 0 ), // creation time
  518. u32( 0 ), // modification time
  519. u32( timescale ),
  520. u32( durationInTimescale ),
  521. u32( 0x00010000 ), // rate (1.0)
  522. u16( 0x0100 ), // volume (1.0)
  523. new Uint8Array( 10 ), // reserved
  524. // matrix
  525. u32( 0x00010000 ), u32( 0 ), u32( 0 ),
  526. u32( 0 ), u32( 0x00010000 ), u32( 0 ),
  527. u32( 0 ), u32( 0 ), u32( 0x40000000 ),
  528. new Uint8Array( 24 ), // pre-defined
  529. u32( 2 ) // next track id
  530. );
  531. // moov
  532. const moov = box( 'moov', mvhd, trak );
  533. // Calculate actual mdat offset and update stco
  534. const mdatOffset = ftyp.length + moov.length;
  535. const moovArray = new Uint8Array( moov );
  536. // Find and update stco offset (search for 'stco' in moov)
  537. for ( let i = 0; i < moovArray.length - 16; i ++ ) {
  538. if ( moovArray[ i ] === 0x73 && moovArray[ i + 1 ] === 0x74 &&
  539. moovArray[ i + 2 ] === 0x63 && moovArray[ i + 3 ] === 0x6F ) {
  540. // Found 'stco', offset value is at i + 12
  541. const offset = mdatOffset + 8;
  542. moovArray[ i + 12 ] = ( offset >> 24 ) & 0xFF;
  543. moovArray[ i + 13 ] = ( offset >> 16 ) & 0xFF;
  544. moovArray[ i + 14 ] = ( offset >> 8 ) & 0xFF;
  545. moovArray[ i + 15 ] = offset & 0xFF;
  546. break;
  547. }
  548. }
  549. // Update mdat size
  550. const mdatSizeBytes = u32( mdatSize );
  551. // Combine all parts
  552. const result = new Uint8Array( ftyp.length + moovArray.length + mdatSize );
  553. let offset = 0;
  554. result.set( ftyp, offset ); offset += ftyp.length;
  555. result.set( moovArray, offset ); offset += moovArray.length;
  556. result.set( mdatSizeBytes, offset );
  557. result.set( str( 'mdat' ), offset + 4 );
  558. offset += 8;
  559. for ( const chunk of chunks ) {
  560. result.set( chunk.data, offset );
  561. offset += chunk.data.length;
  562. }
  563. return result;
  564. }
  565. function formatFileSize( sizeB, K = 1024 ) {
  566. if ( sizeB === 0 ) return '0B';
  567. const sizes = [ sizeB, sizeB / K, sizeB / K / K ].reverse();
  568. const units = [ 'B', 'KB', 'MB' ].reverse();
  569. const index = sizes.findIndex( size => size >= 1 );
  570. return new Intl.NumberFormat( 'en-us', { useGrouping: true, maximumFractionDigits: 1 } )
  571. .format( sizes[ index ] ) + units[ index ];
  572. }
  573. export { MenubarRender };
粤ICP备19079148号