@ -24,6 +24,7 @@ import useKeyboard from './hooks/useKeyboard';
import useFileFormatState from './hooks/useFileFormatState' ;
import useFrameCapture from './hooks/useFrameCapture' ;
import useSegments from './hooks/useSegments' ;
import useDirectoryAccess from './hooks/useDirectoryAccess' ;
import UserSettingsContext from './contexts/UserSettingsContext' ;
@ -61,8 +62,8 @@ import { shouldCopyStreamByDefault, getAudioStreams, getRealVideoStreams, isAudi
import { exportEdlFile , readEdlFile , saveLlcProject , loadLlcProject , askForEdlImport } from './edlStore' ;
import { formatYouTube , getFrameCountRaw } from './edlFormats' ;
import {
getOutPath , getSuffixedOutPath , handleError , getOutDir , getFileDir ,
checkDirWriteAccess, dirExists , isMasBuild, isStoreBuild , dragPreventer ,
getOutPath , getSuffixedOutPath , handleError , getOutDir ,
isMasBuild, isStoreBuild , dragPreventer ,
filenamify , getOutFileExtension , generateSegFileName , defaultOutSegTemplate ,
havePermissionToReadFile , resolvePathIfNeeded , getPathReadAccessError , html5ifiedPrefix , html5dummySuffix , findExistingHtml5FriendlyFile ,
deleteFiles , isOutOfSpaceError , getNumDigits , isExecaFailure , readFileSize , readFileSizes , checkFileSizes , setDocumentTitle ,
@ -72,7 +73,7 @@ import { formatDuration } from './util/duration';
import { adjustRate } from './util/rate-calculator' ;
import { askExtractFramesAsImages } from './dialogs/extractFrames' ;
import { askForHtml5ifySpeed } from './dialogs/html5ify' ;
import { askForOutDir , askForI nputDir, askForI mportChapters, promptTimeOffset , askForFileOpenAction , confirmExtractAllStreamsDialog , showCleanupFilesDialog , showDiskFull , showExportFailedDialog , showConcatFailedDialog , openYouTubeChaptersDialog , openAbout , showRefuseToOverwrite , openDirToast , openCutFinishedToast , openConcatFinishedToast } from './dialogs' ;
import { askForOutDir , askForI mportChapters, promptTimeOffset , askForFileOpenAction , confirmExtractAllStreamsDialog , showCleanupFilesDialog , showDiskFull , showExportFailedDialog , showConcatFailedDialog , openYouTubeChaptersDialog , openAbout , showRefuseToOverwrite , openDirToast , openCutFinishedToast , openConcatFinishedToast } from './dialogs' ;
import { openSendReportDialog } from './reporting' ;
import { fallbackLng } from './i18n' ;
import { createSegment , getCleanCutSegments , findSegmentsAtCursor , sortSegments , getSegmentTags , convertSegmentsToChapters , hasAnySegmentOverlap , isDurationValid } from './segments' ;
@ -454,66 +455,7 @@ const App = memo(() => {
if ( ! supportsRotation && ! hideAllNotifications ) toast . fire ( { text : i18n . t ( 'Lossless rotation might not work with this file format. You may try changing to MP4' ) } ) ;
} , [ hideAllNotifications , fileFormat ] ) ;
const ensureAccessibleDirectories = useCallback ( async ( { inputPath , checkInputDir } ) => {
/ / M a c O S A p p S t o r e s a n d b o x d o e s n ' t a l l o w w r i t i n g a n y w h e r e , a n d w e s e t t h e f l a g c o m . a p p l e . s e c u r i t y . f i l e s . u s e r - s e l e c t e d . r e a d - w r i t e
/ / W i t h t h i s f l a g , w e c a n s h o w t h e u s e r a n o p e n - d i a l o g f o r a d i r e c t o r y , a n d o n c e t h e u s e r h a s o p e n e d t h a t d i r e c t o r y , w e c a n w r i t e f i l e s t h e r e u n t i l t h e a p p i s r e s t a r t e d .
/ / N O T E : w h e n M A S ( d e v ) b u i l d , A p p l i c a t i o n S u p p o r t w i l l i n s t e a d b e h e r e :
/ / ~ / L i b r a r y / C o n t a i n e r s / n o . m i f i . l o s s l e s s c u t - m a c / D a t a / L i b r a r y / A p p l i c a t i o n S u p p o r t
/ / T o s t a r t f r o m s c r a t c h : r m - r f ~ / L i b r a r y / C o n t a i n e r s / n o . m i f i . l o s s l e s s c u t - m a c
/ / c o n s t s i m u l a t e M a s B u i l d = i s D e v ; / / c a n b e u s e d f o r t e s t i n g t h i s l o g i c w i t h o u t h a v i n g t o b u i l d m a s - d e v
const simulateMasBuild = false ;
const masMode = isMasBuild || simulateMasBuild ;
if ( checkInputDir ) {
/ / C h e c k i n p u t d i r , i f w e n e e d t o w r i t e p r o j e c t f i l e h e r e
const inputFileDir = getFileDir ( inputPath ) ;
let simulateMasPermissionError = simulateMasBuild ;
for ( ; ; ) {
/ / e s l i n t - d i s a b l e - n e x t - l i n e n o - a w a i t - i n - l o o p
if ( await checkDirWriteAccess ( inputFileDir ) && ! simulateMasPermissionError ) break ;
if ( ! masMode ) {
/ / f a i l r i g h t a w a y
errorToast ( i18n . t ( 'You have no write access to the directory of this file' ) ) ;
return { cancel : true } ;
}
/ / W e a r e n o w m a s , s o w e n e e d t o t r y t o f o r c e t h e u s e r t o a l l o w a c c e s s t o t h e d i r , s o w e c a n w r i t e t h e p r o j e c t f i l e l a t e r
/ / e s l i n t - d i s a b l e - n e x t - l i n e n o - a w a i t - i n - l o o p
const userSelectedDir = await askForInputDir ( inputFileDir ) ;
simulateMasPermissionError = false ; / / a s s u m e u s e r c h o s e t h e r i g h t d i r
if ( userSelectedDir == null ) return { cancel : true } ; / / a l l o w u s e r t o c a n c e l
}
}
let newCustomOutDir = customOutDir ;
/ / R e s e t i f d o e s n ' t e x i s t a n y m o r e
const customOutDirExists = await dirExists ( customOutDir ) ;
if ( ! customOutDirExists ) {
setCustomOutDir ( undefined ) ;
newCustomOutDir = undefined ;
}
const effectiveOutDirPath = getOutDir ( newCustomOutDir , inputPath ) ;
const hasDirWriteAccess = await checkDirWriteAccess ( effectiveOutDirPath ) ;
if ( ! hasDirWriteAccess || simulateMasBuild ) {
if ( masMode ) {
const newOutDir = await askForOutDir ( effectiveOutDirPath ) ;
/ / I f u s e r c a n c e l e d o p e n d i a l o g , r e f u s e t o c o n t i n u e , b e c a u s e w e w i l l g e t p e r m i s s i o n d e n i e d e r r o r f r o m M A S s a n d b o x
if ( ! newOutDir ) return { cancel : true } ;
setCustomOutDir ( newOutDir ) ;
newCustomOutDir = newOutDir ;
} else {
errorToast ( i18n . t ( 'You have no write access to the directory of this file, please select a custom working dir' ) ) ;
setCustomOutDir ( undefined ) ;
newCustomOutDir = undefined ;
return { cancel : true } ;
}
}
return { cancel : false , newCustomOutDir } ;
} , [ customOutDir , setCustomOutDir ] ) ;
const { ensureWritableDirs } = useDirectoryAccess ( { customOutDir , setCustomOutDir } ) ;
const toggleCaptureFormat = useCallback ( ( ) => setCaptureFormat ( f => ( f === 'png' ? 'jpeg' : 'png' ) ) , [ setCaptureFormat ] ) ;
@ -678,6 +620,7 @@ const App = memo(() => {
const { waveforms } = useWaveform ( { filePath , commandedTime , zoomedDuration , waveformEnabled , mainAudioStream , shouldShowWaveform , ffmpegExtractWindow } ) ;
const resetState = useCallback ( ( ) => {
console . log ( 'State reset' ) ;
const video = videoRef . current ;
setCommandedTime ( 0 ) ;
video . currentTime = 0 ;
@ -760,8 +703,7 @@ const App = memo(() => {
setPreviewFilePath ( path ) ;
setUsingDummyVideo ( usesDummyVideo ) ;
showUnsupportedFileMessage ( ) ;
} , [ html5ify , html5ifyDummy , showUnsupportedFileMessage ] ) ;
} , [ html5ify , html5ifyDummy ] ) ;
const convertFormatBatch = useCallback ( async ( ) => {
if ( batchFiles . length < 1 ) return ;
@ -783,7 +725,7 @@ const App = memo(() => {
for ( const path of filePaths ) {
try {
/ / e s l i n t - d i s a b l e - n e x t - l i n e n o - a w a i t - i n - l o o p
const { newCustomOutDir , cancel } = await ensure AccessibleDirectorie s( { inputPath : path } ) ;
const { newCustomOutDir , cancel } = await ensure WritableDir s( { inputPath : path } ) ;
if ( cancel ) return ;
/ / e s l i n t - d i s a b l e - n e x t - l i n e n o - a w a i t - i n - l o o p
@ -805,7 +747,7 @@ const App = memo(() => {
setWorking ( ) ;
setCutProgress ( ) ;
}
} , [ batchFiles , ensure AccessibleDirectorie s, html5ify , setWorking ] ) ;
} , [ batchFiles , ensure WritableDir s, html5ify , setWorking ] ) ;
const getConvertToSupportedFormat = useCallback ( ( fallback ) => rememberConvertToSupportedFormat || fallback , [ rememberConvertToSupportedFormat ] ) ;
@ -922,7 +864,7 @@ const App = memo(() => {
const firstPath = paths [ 0 ] ;
if ( ! firstPath ) return ;
const { newCustomOutDir , cancel } = await ensure AccessibleDirectorie s( { inputPath : firstPath } ) ;
const { newCustomOutDir , cancel } = await ensure WritableDir s( { inputPath : firstPath } ) ;
if ( cancel ) return ;
const outDir = getOutDir ( newCustomOutDir , firstPath ) ;
@ -975,7 +917,7 @@ const App = memo(() => {
setWorking ( ) ;
setCutProgress ( ) ;
}
} , [ setWorking , ensure AccessibleDirectorie s, segmentsToChapters , concatFiles , ffmpegExperimental , preserveMovData , movFastStart , preserveMetadataOnMerge , closeBatch , hideAllNotifications , handleConcatFailed ] ) ;
} , [ setWorking , ensure WritableDir s, segmentsToChapters , concatFiles , ffmpegExperimental , preserveMovData , movFastStart , preserveMetadataOnMerge , closeBatch , hideAllNotifications , handleConcatFailed ] ) ;
const cleanupFiles = useCallback ( async ( cleanupChoices2 ) => {
/ / S t o r e p a t h s b e f o r e w e r e s e t s t a t e
@ -1291,25 +1233,7 @@ const App = memo(() => {
loadCutSegments ( await readEdlFile ( { type , path } ) , append ) ;
} , [ loadCutSegments ] ) ;
const loadMedia = useCallback ( async ( { filePath : fp , customOutDir : cod , projectPath } ) => {
console . log ( 'loadMedia' , fp , cod , projectPath ) ;
resetState ( ) ;
console . log ( 'state reset' ) ;
setWorking ( i18n . t ( 'Loading file' ) ) ;
async function checkAndSetExistingHtml5FriendlyFile ( ) {
const res = await findExistingHtml5FriendlyFile ( fp , cod ) ;
if ( ! res ) return false ;
console . log ( 'Found existing supported file' , res . path ) ;
setUsingDummyVideo ( res . usingDummyVideo ) ;
setPreviewFilePath ( res . path ) ;
showPreviewFileLoadedMessage ( basename ( res . path ) ) ;
return true ;
}
const loadMedia = useCallback ( async ( { filePath : fp , projectPath } ) => {
async function tryOpenProjectPath ( path , type ) {
if ( ! ( await exists ( path ) ) ) return false ;
await loadEdlFile ( { path , type } ) ;
@ -1339,14 +1263,30 @@ const App = memo(() => {
}
}
setWorking ( i18n . t ( 'Loading file' ) ) ;
try {
/ / N e e d t o c h e c k i f f i l e i s a c t u a l l y r e a d a b l e
const pathReadAccessErrorCode = await getPathReadAccessError ( fp ) ;
if ( pathReadAccessErrorCode != null ) {
let errorMessage ;
if ( pathReadAccessErrorCode === 'ENOENT' ) errorMessage = i18n . t ( 'The media you tried to open does not exist' ) ;
else if ( [ 'EACCES' , 'EPERM' ] . includes ( pathReadAccessErrorCode ) ) errorMessage = i18n . t ( 'You do not have permission to access this file' ) ;
else errorMessage = i18n . t ( 'Could not open media due to error {{errorCode}}' , { errorCode : pathReadAccessErrorCode } ) ;
errorToast ( errorMessage ) ;
return ;
}
/ / N o t s u r e w h y t h i s o n e i s n e e d e d , b u t I t h i n k s o m e t i m e s f s . a c c e s s d o e s n ' t f a i l b u t i t f a i l s w h e n a c t u a l l y t r y i n g t o r e a d
if ( ! ( await havePermissionToReadFile ( fp ) ) ) {
errorToast ( i18n . t ( 'You do not have permission to access this file' ) ) ;
return ;
}
const fileMeta = await readFileMeta ( fp ) ;
/ / c o n s o l e . l o g ( ' f i l e m e t a r e a d ' , f i l e M e t a ) ;
const fileFormatNew = await getSmarterOutFormat ( { filePath : fp , fileMeta } ) ;
/ / c o n s o l e . l o g ( s t r e a m s , f i l e M e t a . f o r m a t , f i l e F o r m a t ) ;
if ( ! fileFormatNew ) throw new Error ( 'Unable to determine file format' ) ;
const timecode = autoLoadTimecode ? getTimecodeFromStreams ( fileMeta . streams ) : undefined ;
@ -1364,21 +1304,35 @@ const App = memo(() => {
stream . index , shouldCopyStreamByDefault ( stream ) ,
] ) ) ;
if ( timecode ) setStartTimeOffset ( timecode ) ;
const validDuration = isDurationValid ( parseFloat ( fileMeta . format . duration ) ) ;
setDetectedFps ( haveVideoStream ? getStreamFps ( videoStream ) : undefined ) ;
const hevcPlaybackSupported = enableNativeHevc && await hevcPlaybackSupportedPromise ;
if ( isAudioDefinitelyNotSupported ( fileMeta . streams ) ) {
toast . fire ( { icon : 'info' , text : i18n . t ( 'The audio track is not supported. You can convert to a supported format from the menu' ) } ) ;
}
const mightNeedAutoHtml5ify = ! willPlayerProperlyHandleVideo ( { streams : fileMeta . streams , hevcPlaybackSupported } ) && validDuration ;
const validDuration = isDurationValid ( parseFloat ( fileMeta . format . duration ) ) ;
const hasLoadedExistingHtml5FriendlyFile = await checkAndSetExistingHtml5FriendlyFile ( ) ;
/ / W e m a y b e b e w r i t i n g p r o j e c t f i l e t o i n p u t p a t h ' s d i r ( i f s t o r e P r o j e c t I n W o r k i n g D i r i s t r u e ) , o r w r i t e h t m l 5 i f i e d f i l e t o i n p u t d i r
const { newCustomOutDir : cod , canceled } = await ensureWritableDirs ( { inputPath : fp , checkInputDir : ! storeProjectInWorkingDir || mightNeedAutoHtml5ify } ) ;
if ( canceled ) return ;
const hevcPlaybackSupported = enableNativeHevc && await hevcPlaybackSupportedPromise ;
const existingHtml5FriendlyFile = await findExistingHtml5FriendlyFile ( fp , cod ) ;
const needsAutoHtml5ify = ! existingHtml5FriendlyFile && mightNeedAutoHtml5ify ;
/ / B E G I N S T A T E U P D A T E S :
console . log ( 'loadMedia' , fp , cod , projectPath ) ;
resetState ( ) ;
if ( existingHtml5FriendlyFile ) {
console . log ( 'Found existing html5 friendly file' , existingHtml5FriendlyFile . path ) ;
setUsingDummyVideo ( existingHtml5FriendlyFile . usingDummyVideo ) ;
setPreviewFilePath ( existingHtml5FriendlyFile . path ) ;
}
/ / ' f a s t e s t ' w o r k s w i t h a l m o s t a l l v i d e o f i l e s
if ( ! hasLoadedExistingHtml5FriendlyFile && ! willPlayerProperlyHandleVideo ( { streams : fileMeta . streams , hevcPlaybackSupported } ) && validDuration ) {
if ( needsAutoHtml5ify ) {
/ / T r y t o a u t o - h t m l 5 i f y i f t h e r e a r e k n o w n i s s u e s w i t h t h i s f i l e
/ / ' f a s t e s t ' w o r k s w i t h a l m o s t a l l v i d e o f i l e s
await html5ifyAndLoadWithPreferences ( cod , fp , 'fastest' , haveVideoStream , haveAudioStream ) ;
}
@ -1386,8 +1340,8 @@ const App = memo(() => {
/ / t h r o w n e w E r r o r ( ' t e s t ' ) ;
if ( ! validDuration ) toast . fire ( { icon : 'warning' , timer : 10000 , text : i18n . t ( 'This file does not have a valid duration. This may cause issues. You can try to fix the file\'s duration from the File menu' ) } ) ;
if ( timecode ) setStartTimeOffset ( timecode ) ;
setDetectedFps ( haveVideoStream ? getStreamFps ( videoStream ) : undefined ) ;
setMainFileMeta ( { streams : fileMeta . streams , formatData : fileMeta . format , chapters : fileMeta . chapters } ) ;
setMainVideoStream ( videoStream ) ;
setMainAudioStream ( audioStream ) ;
@ -1395,6 +1349,17 @@ const App = memo(() => {
setFileFormat ( outFormatLocked || fileFormatNew ) ;
setDetectedFileFormat ( fileFormatNew ) ;
/ / o n l y s h o w o n e t o a s t , o r e l s e w e w i l l o n l y s h o w t h e l a s t o n e
if ( existingHtml5FriendlyFile ) {
showPreviewFileLoadedMessage ( basename ( existingHtml5FriendlyFile . path ) ) ;
} else if ( needsAutoHtml5ify ) {
showUnsupportedFileMessage ( ) ;
} else if ( isAudioDefinitelyNotSupported ( fileMeta . streams ) ) {
toast . fire ( { icon : 'info' , text : i18n . t ( 'The audio track is not supported. You can convert to a supported format from the menu' ) } ) ;
} else if ( ! validDuration ) {
toast . fire ( { icon : 'warning' , timer : 10000 , text : i18n . t ( 'This file does not have a valid duration. This may cause issues. You can try to fix the file\'s duration from the File menu' ) } ) ;
}
/ / T h i s n e e d s t o b e l a s t , b e c a u s e i t t r i g g e r s < v i d e o > t o l o a d t h e v i d e o
/ / I f n o t , o n V i d e o E r r o r m i g h t b e t r i g g e r e d b e f o r e s e t W o r k i n g ( ) h a s b e e n c l e a r e d .
/ / h t t p s : / / g i t h u b . c o m / m i f i / l o s s l e s s - c u t / i s s u e s / 5 1 5
@ -1403,7 +1368,7 @@ const App = memo(() => {
resetState ( ) ;
throw err ;
}
} , [ resetState, setWorking , showPreviewFileLoadedMessage , loadEdlFile , getEdlFilePath , getEdlFilePathOld , enableAskForImportChapters , loadCutSegments , autoLoadTimecode , enableNativeHevc , setCopyStreamIdsForPath , setFileFormat , outFormatLocked , setDetectedFileFormat , html5ifyAndLoadWithPreferences] ) ;
} , [ ensureWritableDirs, storeProjectInWorkingDir , resetState , setWorking , loadEdlFile , getEdlFilePath , getEdlFilePathOld , enableAskForImportChapters , loadCutSegments , autoLoadTimecode , enableNativeHevc , setCopyStreamIdsForPath , setFileFormat , outFormatLocked , setDetectedFileFormat , showPreviewFileLoadedMessage, html5ifyAndLoadWithPreferences, showUnsupportedFileMessage ] ) ;
const toggleLastCommands = useCallback ( ( ) => setLastCommandsVisible ( val => ! val ) , [ ] ) ;
const toggleSettings = useCallback ( ( ) => setSettingsVisible ( val => ! val ) , [ ] ) ;
@ -1439,28 +1404,8 @@ const App = memo(() => {
return ;
}
const pathReadAccessErrorCode = await getPathReadAccessError ( path ) ;
if ( pathReadAccessErrorCode != null ) {
let errorMessage ;
if ( pathReadAccessErrorCode === 'ENOENT' ) errorMessage = i18n . t ( 'The media you tried to open does not exist' ) ;
else if ( [ 'EACCES' , 'EPERM' ] . includes ( pathReadAccessErrorCode ) ) errorMessage = i18n . t ( 'You do not have permission to access this file' ) ;
else errorMessage = i18n . t ( 'Could not open media due to error {{errorCode}}' , { errorCode : pathReadAccessErrorCode } ) ;
errorToast ( errorMessage ) ;
return ;
}
/ / N o t s u r e w h y t h i s o n e i s n e e d e d , b u t I t h i n k s o m e t i m e s f s . a c c e s s d o e s n ' t f a i l b u t i t f a i l s w h e n a c t u a l l y t r y i n g t o r e a d
if ( ! ( await havePermissionToReadFile ( path ) ) ) {
errorToast ( i18n . t ( 'You do not have permission to access this file' ) ) ;
return ;
}
/ / c h e c k I n p u t D i r : t r u e b e c a u s e w e m a y b e b e w r i t i n g p r o j e c t f i l e h e r e ( i f s t o r e P r o j e c t I n W o r k i n g D i r i s t r u e )
const { newCustomOutDir , cancel } = await ensureAccessibleDirectories ( { inputPath : path , checkInputDir : ! storeProjectInWorkingDir } ) ;
if ( cancel ) return ;
await loadMedia ( { filePath : path , customOutDir : newCustomOutDir , projectPath } ) ;
} , [ ensureAccessibleDirectories , loadMedia , storeProjectInWorkingDir ] ) ;
await loadMedia ( { filePath : path , projectPath } ) ;
} , [ loadMedia ] ) ;
/ / t o d o m e r g e w i t h u s e r O p e n F i l e s ?
const batchOpenSingleFile = useCallback ( async ( path ) => {
@ -1592,7 +1537,8 @@ const App = memo(() => {
setCutProgress ( 0 ) ;
const path = await fixInvalidDuration ( { fileFormat , customOutDir , duration , onProgress : setCutProgress } ) ;
toast . fire ( { icon : 'info' , text : i18n . t ( 'Duration has been fixed' ) } ) ;
await loadMedia ( { filePath : path , customOutDir } ) ;
await loadMedia ( { filePath : path } ) ;
} catch ( err ) {
errorToast ( i18n . t ( 'Failed to fix file duration' ) ) ;
console . error ( 'Failed to fix file duration' , err ) ;
@ -1953,9 +1899,11 @@ const App = memo(() => {
if ( hasVideo ) {
/ / " f a s t e s t " i s t h e m o s t l i k e l y t y p e n o t t o f a i l f o r v i d e o ( b u t i t i s m u t e d ) .
await html5ifyAndLoadWithPreferences ( customOutDir , filePath , 'fastest' , hasVideo , hasAudio ) ;
showUnsupportedFileMessage ( ) ;
} else if ( hasAudio ) {
/ / F o r a u d i o d o a f a s t r e - e n c o d e
await html5ifyAndLoadWithPreferences ( customOutDir , filePath , 'fastest-audio' , hasVideo , hasAudio ) ;
showUnsupportedFileMessage ( ) ;
}
} catch ( err ) {
console . error ( err ) ;
@ -1966,7 +1914,7 @@ const App = memo(() => {
} catch ( err ) {
handleError ( err ) ;
}
} , [ fileUri , usingPreviewFile , hasVideo, hasAudio , html5ifyAndLoadWithPreferences , customOutDir , filePath, setWorking ] ) ;
} , [ fileUri , usingPreviewFile , filePath, setWorking , hasVideo, hasAudio , html5ifyAndLoadWithPreferences , customOutDir , showUnsupportedFileMessage ] ) ;
useEffect ( ( ) => {
async function exportEdlFile2 ( e , type ) {