@@ -595,124 +595,34 @@ export default class SessionEditor {
595595 } ) ;
596596 }
597597
598- async mergeVideoTracks (
598+ async mergeVideoAudioTracks (
599599 progress : Progress ,
600600 abortController : AbortController ,
601601 deleteOld ?: boolean ,
602602 ) : Promise < t . SessionChange | undefined > {
603603 assert ( this . session . isLoaded ( ) ) ;
604604
605- const individualFilesProgressMultiplier = 0.9 ;
606-
607- if ( this . session . body . videoTracks . length === 0 ) return ;
608-
609- const tempDir = path . join ( this . session . core . dataPath , 'temp' ) ;
610- await fs . promises . rm ( tempDir , { recursive : true , force : true } ) ;
611- await fs . promises . mkdir ( tempDir , { recursive : true } ) ;
612-
613- const sortedVideoTracks = _ . orderBy ( this . session . body . videoTracks , t => t . clockRange . start ) ;
614- const videoFiles : string [ ] = [ ] ;
615- for ( const [ i , t ] of sortedVideoTracks . entries ( ) ) {
616- if ( abortController . signal . aborted ) return ;
617-
618- progress . report ( { message : t . title } ) ;
619- assert ( t . file . type === 'blob' ) ;
620-
621- const startOfNext = sortedVideoTracks [ i + 1 ] ?. clockRange . start ?? this . session . head . duration ;
622- const gap = startOfNext - t . clockRange . end ;
623-
624- const origFilePath = path . join ( this . session . core . dataPath , 'blobs' , t . file . sha1 ) ;
625- const outFilePath = path . join ( tempDir , t . title + '-' + ( i + 1 ) ) ;
626-
627- console . log ( 'ffmpeg out: ' , outFilePath , 'gap: ' , gap ) ;
628-
629- if ( gap > 0 ) {
630- const vFilter = gap > 0 ? `tpad=stop_mode=clone:stop_duration=${ gap } ` : 'null' ;
631- const args = [
632- '-y' ,
633- '-i' ,
634- origFilePath ,
635- '-filter:v' ,
636- vFilter ,
637- '-map' ,
638- '0:v' ,
639- '-an' ,
640- '-c:v' ,
641- 'libx264' ,
642- '-preset' ,
643- 'ultrafast' ,
644- '-movflags' ,
645- '+faststart' ,
646- '-f' ,
647- 'mp4' ,
648- outFilePath ,
649- ] ;
650- if ( config . debug ) {
651- console . log ( 'ffmpeg ' + args . join ( ' ' ) ) ;
652- }
653- const { stdout, stderr } = await execFile ( 'ffmpeg' , args ) ;
654- if ( config . debug && stderr . trim ( ) ) console . error ( stderr ) ;
655- videoFiles . push ( outFilePath ) ;
656- } else if ( gap < 0 ) {
657- throw new Error ( 'Found overlapping videos' ) ;
658- } else {
659- videoFiles . push ( origFilePath ) ;
660- }
605+ let limitRange : t . ClockRange ;
606+ const editorSelection = this . selection ?. type === 'editor' && {
607+ start : Math . min ( this . selection . focus , this . selection . anchor ) ,
608+ end : Math . max ( this . selection . focus , this . selection . anchor ) ,
609+ } ;
661610
662- progress . report ( {
663- message : t . title ,
664- increment : ( 1 / sortedVideoTracks . length ) * individualFilesProgressMultiplier * 100 ,
665- } ) ;
611+ if ( editorSelection && editorSelection . end > editorSelection . start ) {
612+ limitRange = editorSelection ;
613+ } else {
614+ limitRange = { start : 0 , end : this . session . head . duration } ;
666615 }
667616
668- if ( abortController . signal . aborted ) return ;
669-
670- progress . report ( { message : 'Final output' } ) ;
671-
672- const videoFilesStr = videoFiles . map ( f => `file ${ f } ` . replace ( / ' / g, "'\\''" ) ) . join ( '\n' ) ;
673- const videoFilesListPath = path . join ( tempDir , 'list' ) ;
674- await fs . promises . writeFile ( videoFilesListPath , videoFilesStr , 'utf8' ) ;
675- const finalOutFilePath = path . join ( tempDir , 'final-output.mp4' ) ;
676-
677- const concatArgs = [
678- '-y' ,
679- '-f' ,
680- 'concat' ,
681- '-safe' ,
682- '0' ,
683- '-i' ,
684- videoFilesListPath ,
685- '-an' , // no audio
686- '-c:v' ,
687- 'libx264' , // reencode video with H.264
688- '-movflags' ,
689- '+faststart' ,
690- '-f' ,
691- 'mp4' , // force MP4 container
692- finalOutFilePath ,
693- ] ;
694- if ( config . debug ) console . log ( 'ffmpeg ' + concatArgs . join ( ' ' ) ) ;
695- await execFile ( 'ffmpeg' , concatArgs ) ;
696- progress . report ( { message : 'Done' , increment : ( 1 - individualFilesProgressMultiplier ) * 100 } ) ;
697-
698- // TODO use stream.
699- const finalData = await fs . promises . readFile ( finalOutFilePath ) ;
700- const sha1 = await misc . computeSHA1 ( finalData ) ;
701- await this . session . core . copyToBlob ( finalOutFilePath , sha1 ) ;
702- await fs . promises . rm ( tempDir , { recursive : true , force : true } ) ;
703-
704- const finalVideoTrack : t . VideoTrack = {
705- id : uuid ( ) ,
706- clockRange : { start : sortedVideoTracks [ 0 ] . clockRange . start , end : this . session . head . duration } ,
707- title : 'Merged videos' ,
708- type : 'video' ,
709- file : { type : 'blob' , sha1 } ,
710- } ;
617+ const finalVideoTrack = await this . mergeAudioAndVideoTracksHelper ( progress , abortController , limitRange ) ;
711618
712- if ( abortController . signal . aborted ) return ;
619+ if ( ! finalVideoTrack ) return ;
713620
714621 return this . insertApplySessionPatch ( {
715- body : { videoTracks : deleteOld ? [ finalVideoTrack ] : [ ...this . session . body . videoTracks , finalVideoTrack ] } ,
622+ body : {
623+ videoTracks : deleteOld ? [ finalVideoTrack ] : [ ...this . session . body . videoTracks , finalVideoTrack ] ,
624+ audioTracks : deleteOld ? [ ] : this . session . body . audioTracks ,
625+ } ,
716626 effects : [ { type : 'media' } ] ,
717627 } ) ;
718628 }
@@ -724,6 +634,31 @@ export default class SessionEditor {
724634 const start = Math . min ( this . selection . anchor , this . selection . focus ) ;
725635 const end = Math . max ( this . selection . anchor , this . selection . focus ) ;
726636 const limitRange : t . ClockRange = { start, end } ;
637+
638+ const finalVideoTrack = await this . mergeAudioAndVideoTracksHelper ( progress , abortController , limitRange ) ;
639+
640+ if ( ! finalVideoTrack ) return ;
641+
642+ const change1 = this . insertApplySessionPatch ( {
643+ head : { isClip : true } ,
644+ body : { videoTracks : [ finalVideoTrack ] , audioTracks : [ ] } ,
645+ effects : [ { type : 'media' } ] ,
646+ } ) ;
647+
648+ const mergeRange : t . ClockRange = { start : 0 , end : limitRange . start } ;
649+ const change2 = this . merge ( mergeRange , true ) ;
650+ const change3 = this . crop ( limitRange . end - limitRange . start , true ) ;
651+
652+ return [ change1 , change2 , change3 ] ;
653+ }
654+
655+ async mergeAudioAndVideoTracksHelper (
656+ progress : Progress ,
657+ abortController : AbortController ,
658+ limitRange : t . ClockRange ,
659+ ) : Promise < t . RangedTrackFile | undefined > {
660+ assert ( this . session . isLoaded ( ) ) ;
661+
727662 const tempDir = path . join ( this . session . core . dataPath , 'temp' ) ;
728663 const blobDir = path . join ( this . session . core . dataPath , 'blobs' ) ;
729664
@@ -750,22 +685,12 @@ export default class SessionEditor {
750685 const finalVideoTrack : t . VideoTrack = {
751686 id : uuid ( ) ,
752687 clockRange : { start : limitRange . start , end : limitRange . end } ,
753- title : 'Merged videos ' ,
688+ title : 'Merged' ,
754689 type : 'video' ,
755690 file : { type : 'blob' , sha1 } ,
756691 } ;
757692
758- const change1 = this . insertApplySessionPatch ( {
759- head : { isClip : true } ,
760- body : { videoTracks : [ finalVideoTrack ] , audioTracks : [ ] } ,
761- effects : [ { type : 'media' } ] ,
762- } ) ;
763-
764- const mergeRange : t . ClockRange = { start : 0 , end : limitRange . start } ;
765- const change2 = this . merge ( mergeRange , true ) ;
766- const change3 = this . crop ( limitRange . end - limitRange . start , true ) ;
767-
768- return [ change1 , change2 , change3 ] ;
693+ return finalVideoTrack ;
769694 }
770695
771696 /**
0 commit comments