diff --git a/ffprobe.ts b/ffprobe.ts new file mode 100644 index 00000000..847bccf2 --- /dev/null +++ b/ffprobe.ts @@ -0,0 +1,781 @@ +// FFProbe response types. Copied from https://gist.github.com/termermc/2a62735201cede462763456542d8a266 +// See also https://github.com/DefinitelyTyped/DefinitelyTyped/blob/a21bbc63c5a31afbad57c3830582c389d32a931b/types/ffprobe/index.d.ts#L4 + +// These definitions are my best attempt at documenting the output of FFprobe using the "-print_format json" option +// The descriptions come mostly out of experience, deduction, and the very sparse documentation of the outputs +// Not all fields will be present (it depends on the file), but fields that are definite are not marked as optional +// Sample probe: +// ffprobe -v quiet -print_format json -show_format -show_streams -show_chapters -show_error my_file.mp4 + +/** + * @typedef FFprobeDisposition + * @property {number} default 1 if the default track + * @property {number} dub 1 if a dub track + * @property {number} original 1 if the original track + * @property {number} comment 1 if a comment track + * @property {number} lyrics 1 if a lyrics track + * @property {number} karaoke 1 if a karaoke track + * @property {number} forced 1 if a forced track + * @property {number} hearing_impaired 1 if a track for the hearing impaired + * @property {number} visual_impaired 1 if a track for the visually impaired + * @property {number} clean_effects 1 if a clean effects track (meaning not entirely understood) + * @property {number} attached_pic 1 if an attached picture track + * @property {number} timed_thumbnails 1 if a timed thumbnails track (perhaps like the preview thumbnails you get when scrolling over a YouTube video's seek bar) + * @property {number} captions 1 if a captions track + * @property {number} descriptions 1 if a descriptions track + * @property {number} metadata 1 if a metadata track + * @property {number} dependent 1 if a dependent track (unclear meaning) + * @property {number} still_image 1 if a still image track + */ + +/** + * @typedef FFprobeStreamTags + * @property {string} [language] The track's language code (usually represented using a 3 letter language code, e.g.: "eng") + * @property {string} [handler_name] The name of the handler which produced the track + * @property {string} [vendor_id] The ID of the vendor which produced the track + * @property {string} [encoder] The name of the encoder responsible for creating the stream + * @property {string} [creation_time] The date (often ISO-formatted, but it may use other formats) when the media was created + * @property {string} [comment] The comment attached to the stream + */ + +/** + * @typedef FFprobeStream + * @property {number} index The stream index + * @property {string} codec_name The codec's name + * @property {string} codec_long_name The codec's long (detailed) name + * @property {string} profile The codec profile + * @property {'video'|'audio'|'subtitle'} codec_type The type of codec (video, audio, subtitle, etc) + * @property {string} codec_tag_string The codec tag (technical name) + * @property {string} codec_tag The codec tag ID + * @property {string} [sample_fmt] The audio sample format (not present if codec_type is not "audio") + * @property {string} [sample_rate] A string representation of an integer showing the audio sample rate (not present if codec_type is not "audio") + * @property {number} [channels] The audio track's channel count (not present if codec_type is not "audio") + * @property {'stereo'|'mono'} [channel_layout] The audio track's channel layout (e.g. "stereo") (not present if codec_type is not "audio") + * @property {number} [bits_per_sample] Bits per audio sample (might not be accurate, may just be 0) (not present if codec_type is not "audio") + * @property {number} [width] The video stream width (also available for images) (not present if codec_type is not "video") + * @property {number} [height] The stream height (also available for images) (not present if codec_type is not "video") + * @property {number} [coded_width] The stream's coded width (shouldn't vary from "width") (not present if codec_type is not "video") + * @property {number} [coded_height] The stream's coded height (shouldn't vary from "height") (not present if codec_type is not "video") + * @property {number} [closed_captions] Set to 1 if closed captions are present in stream... I think (not present if codec_type is not "video") + * @property {number} [has_b_frames] Set to 1 if the stream has b-frames... I think (not present if codec_type is not "video") + * @property {string} [sample_aspect_ratio] The sample aspect ratio (you probably want "display_aspect_ratio") (not present if codec_type is not "video") + * @property {string} [display_aspect_ratio] The display (real) aspect ratio (e.g. "16:9") (not present if codec_type is not "video") + * @property {string} [pix_fmt] The pixel format used (not present if codec_type is not "video") + * @property {number} [level] Unknown (not present if codec_type is not "video") + * @property {string} [color_range] The color range used (e.g. "tv") (not present if codec_type is not "video") + * @property {string} [color_space] The color space used (not present if codec_type is not "video") + * @property {string} [color_transfer] The color transfer used (not present if codec_type is not "video") + * @property {string} [color_primaries] The color primaries used (not present if codec_type is not "video") + * @property {string} [chroma_location] The chroma location (not present if codec_type is not "video") + * @property {number} [refs] Unknown (not present if codec_type is not "video") + * @property {'true'|'false'} [is_avc] Whether the stream is AVC (not present if codec_type is not "video") + * @property {string} [nal_length_size] Unknown string representing a number (not present if codec_type is not "video") + * @property {string} r_frame_rate Odd formatting of the frame rate, possibly "real frame rate"? (e.g. "30/1") + * @property {string} avg_frame_rate Odd formatting of the average frame rate (e.g. "30/1") + * @property {string} time_base The division equation to use for converting integer representations of timestamps into seconds (e.g. "1/30000" turns 80632552 into 2687.751733 seconds) + * @property {number} start_pts Unknown + * @property {string} start_time A string representation of a floating point integer showing the start time in seconds + * @property {number} duration_ts The stream's duration in integer timestamp format (defined by time_base) + * @property {string} duration A string representation of a floating point integer showing the stream duration in seconds + * @property {string} [bit_rate] The string representation of an integer showing the stream bit rate (not present on lossless formats such as FLAC) + * @property {string} [bits_per_raw_sample] A string representation of an integer showing the bits per raw sample (not present if codec_type is not "video") + * @property {string} nb_frames A string representation of an integer showing the total number of frames in the stream + * @property {FFprobeDisposition} disposition The stream's disposition + * @property {FFprobeStreamTags} [tags] The stream's tags + */ + +/** + * @typedef FFprobeChapterTags + * @property {string} title The chapter title + */ + +/** + * @typedef FFprobeChapter + * @property {number} id The chapter ID + * @property {string} time_base The division equation to use for converting integer representations of timestamps into seconds (e.g. "1/30000" turns 80632552 into 2687.751733 seconds) + * @property {number} start When the chapter starts in integer timestamp format (defined by time_base) + * @property {string} start_time The string representation of a floating point integer showing when the chapter starts in seconds + * @property {number} end When the chapter end in integer timestamp format (defined by time_base) + * @property {string} end_time The string representation of a floating point integer showing when the chapter ends in seconds + * @property {FFprobeChapterTags} tags The chapter's tags + */ + +/** + * @typedef FFprobeFormatTags + * @property {string} major_brand Not clear, probably the media type brand, but not sure + * @property {string} minor_version The brand version perhaps, but not sure + * @property {string} compatible_brands The brands that are compatible with the referenced brands perhaps, but not sure + * @property {string} [title] The media's title (song metadata uses an all uppercase version) + * @property {string} [artist] The media artist (song metadata uses an all uppercase version) + * @property {string} [date] The media's creation date, seems to be in YYYYMMDD format (song metadata uses an all uppercase version) + * @property {string} [encoder] The name of the encoder responsible for encoding the media + * @property {string} [comment] The comment attached to the file + * @property {string} [description] The description attached to the file + * @property {string} [creation_time] The ISO-formatted date (although it may use other formats) when the media was created + * @property {string} [ALBUM] The album (only present in audio files) + * @property {string} [album_artist] The album arist (only present in audio files) + * @property {string} [ALBUMARTISTSORT] The album artist name used for sorting probably (only present in audio files) + * @property {string} [ARTIST] The song artist (only present in audio files) + * @property {string} [DATE] The date when the song was created (no particular format, often the year) (only present in audio files) + * @property {string} [disc] The string representation of an integer showing the song's disc number (only present in audio files) + * @property {string} [DISCTOTAL] The string representation of an integer showing the total number of discs comprising the album the song is in (only present in audio files) + * @property {string} [ISRC] The song's International Standard Recording Code + * @property {string} [GENRE] The song's genre (only present in audio files) + * @property {string} [TITLE] The song's title (only present in audio files) + * @property {string} [track] The string representation of an integer showing the song's track number (only present in audio files) + * @property {string} [TRACKTOTAL] The string representation of an integer showing the total number of tracks in the album the song is in (only present in audio files) + * @property {string} [YEAR] The string representation of an integer showing the year the song was created (only present in audio files) + * @property {string} [BPM] The string representation of an integer showing the song's BPM (only present in audio files) + * @property {string} [PUBLISHER] The song's publisher (only present in audio files) + */ + +/** + * @typedef FFprobeFormat + * @property {string} filename The path of the probed file (as specified in the input file argument) + * @property {number} nb_streams The total number of streams present + * @property {number} nb_programs The total number of programs present + * @property {string} format_name The name of the format (a comma separated list of applicable file extensions for the format) + * @property {string} format_long_name The long (detailed) name of the format + * @property {string} start_time The string representation of a floating point integer showing the file's starting time + * @property {string} duration The string representation of a floating point integer showing the file's duration in seconds (seems to be a non-accurate, rounded version of the real duration) + * @property {string} size The string representation of a long integer showing the file's size in bytes + * @property {string} bit_rate The string representation of a long integer showing the file's stated bitrate (may vary between streams, probably applies to just video if a video file) + * @property {number} probe_score A score of how confident FFprobe is of the format, 0 to 100. https://stackoverflow.com/questions/25257986/what-does-probe-score-mean-in-ffprobe-output + * @property {FFprobeFormatTags} [tags] The format's tags + */ + +/** + * @typedef FFprobeProbeError + * @property {number} code The error code + * @property {string} string The error message + */ + +/** + * @typedef FFprobeProbeResult + * @property {Array} [streams] The probed file's streams (-show_streams flag required) + * @property {Array} [chapters] The probed file's chapters (-show_chapters flag required) + * @property {FFprobeFormat} [format] The probed file's format data (-show_format flag required) + * @property {FFprobeProbeError} [error] The error that occurred when trying to probe the file (-show_error flag required) + */ + +/** + * The "disposition" field on an FFprobe response stream object + */ +export interface FFprobeStreamDisposition { + /** + * 1 if the default track + */ + default: 1 | 0, + + /** + * 1 if a dub track + */ + dub: 1 | 0, + + /** + * 1 if the original track + */ + original: 1 | 0, + + /** + * 1 if a comment track + */ + comment: 1 | 0, + + /** + * 1 if a lyrics track + */ + lyrics: 1 | 0, + + /** + * 1 if a karaoke track + */ + karaoke: 1 | 0, + + /** + * 1 if a forced track + */ + forced: 1 | 0, + + /** + * 1 if a track for the hearing impaired + */ + hearing_impaired: 1 | 0, + + /** + * 1 if a track for the visually impaired + */ + visual_impaired: 1 | 0, + + /** + * 1 if a clean effects track + */ + clean_effects: 1 | 0, + + /** + * 1 if an attached picture track + */ + attached_pic: 1 | 0, + + /** + * 1 if a timed thumbnails track (perhaps like the preview thumbnails you get when scrolling over a YouTube video's seek bar) + */ + timed_thumbnails: 1 | 0, + + /** + * 1 if a captions track + */ + captions: 1 | 0, + + /** + * 1 if a descriptions track + */ + descriptions: 1 | 0, + + /** + * 1 if a metadata track + */ + metadata: 1 | 0, + + /** + * 1 if a dependent track (unclear meaning) + */ + dependent: 1 | 0, + + /** + * 1 if a still image track + */ + still_image: 1 | 0 +} + +/** + * The "tags" field on an FFprobe response stream object + */ +export interface FFprobeStreamTags { + /** + * The track's language (usually represented using a 3 letter language code, e.g.: "eng") + */ + language?: string, + + /** + * The name of the handler which produced the track + */ + handler_name?: string, + + /** + * The ID of the vendor which produced the track + */ + vendor_id?: string, + + /** + * The name of the encoder responsible for creating the stream + */ + encoder?: string, + + /** + * The date (often ISO-formatted, but it may use other formats) when the media was created + */ + creation_time?: string, + + /** + * The comment attached to the stream + */ + comment?: string + + rotate?: string, +} + +/** + * An FFprobe response stream object + */ +export interface FFprobeStream { + /** + * The stream index + */ + index: number, + + /** + * The codec's name + */ + codec_name: string, + + /** + * The codec's long (detailed) name + */ + codec_long_name: string, + + /** + * The codec profile + */ + profile: string, + + /** + * The type of codec (video, audio, subtitle, etc.) + */ + codec_type: 'video' | 'audio' | 'subtitle' | 'attachment' | 'data', + + /** + * The codec tag (technical name) + */ + codec_tag_string: string, + + /** + * The codec tag ID + */ + codec_tag: string, + + /** + * The audio sample format (not present if codec_type is not "audio") + */ + sample_fmt?: string, + + /** + * A string representation of an integer showing the audio sample rate (not present if codec_type is not "audio") + */ + sample_rate?: string, + + /** + * The audio track's channel count (not present if codec_type is not "audio") + */ + channels?: number, + + /** + * The audio track's channel layout (e.g. "stereo") (not present if codec_type is not "audio") + */ + channel_layout?: 'stereo' | 'mono', + + /** + * Bits per audio sample (might not be accurate, may just be 0) (not present if codec_type is not "audio") + */ + bits_per_sample?: number, + + /** + * The video stream width (also available for images) (not present if codec_type is not "video") + */ + width?: number, + + /** + * The stream height (also available for images) (not present if codec_type is not "video") + */ + height?: number, + + /** + * The stream's coded width (shouldn't vary from "width") (not present if codec_type is not "video") + */ + coded_width?: number, + + /** + * The stream's coded height (shouldn't vary from "height") (not present if codec_type is not "video") + */ + coded_height?: number, + + /** + * Set to 1 if closed captions are present in stream... I think (not present if codec_type is not "video") + */ + closed_captions?: 1 | 0 | number, + + /** + * Set to 1 if the stream has b-frames... I think (not present if codec_type is not "video") + */ + has_b_frames?: 1 | 0 | number, + + /** + * The sample aspect ratio (you probably want "display_aspect_ratio") (not present if codec_type is not "video") + */ + sample_aspect_ratio?: string, + + /** + * The display (real) aspect ratio (e.g. "16:9") (not present if codec_type is not "video") + */ + display_aspect_ratio?: string, + + /** + * The pixel format used (not present if codec_type is not "video") + */ + pix_fmt?: string, + + /** + * Unknown (not present if codec_type is not "video") + */ + level?: number, + + /** + * The color range used (e.g. "tv") (not present if codec_type is not "video") + */ + color_range?: string, + + /** + * The color space used (not present if codec_type is not "video") + */ + color_space?: string, + + /** + * The color transfer used (not present if codec_type is not "video") + */ + color_transfer?: string, + + /** + * The color primaries used (not present if codec_type is not "video") + */ + color_primaries?: string, + + /** + * The chroma location (not present if codec_type is not "video") + */ + chroma_location?: string, + + /** + * Unknown (not present if codec_type is not "video") + */ + refs?: number, + + /** + * Whether the stream is AVC (not present if codec_type is not "video") + */ + is_avc?: 'true' | 'false', + + /** + * Unknown string representing a number (not present if codec_type is not "video") + */ + nal_length_size?: string, + + /** + * Odd formatting of the frame rate, possibly "real frame rate"? (e.g. "30/1") + */ + r_frame_rate: string, + + /** + * Odd formatting of the average frame rate (e.g. "30/1") + */ + avg_frame_rate: string, + + /** + * The division equation to use for converting integer representations of timestamps into seconds (e.g. "1/30000" turns 80632552 into 2687.751733 seconds) + */ + time_base: string, + + /** + * Unknown + */ + start_pts: number, + + /** + * A string representation of a floating point integer showing the start time in seconds + */ + start_time: string, + + /** + * The stream's duration in integer timestamp format (defined by time_base) + */ + duration_ts: number, + + /** + * A string representation of a floating point integer showing the stream duration in seconds + */ + duration: string, + + /** + * The string representation of an integer showing the stream bit rate (not present on lossless formats such as FLAC) + */ + bit_rate?: string, + + /** + * A string representation of an integer showing the bits per raw sample (not present if codec_type is not "video") + */ + bits_per_raw_sample?: string, + + /** + * A string representation of an integer showing the total number of frames in the stream + */ + nb_frames: string, + + /** + * The stream's disposition + */ + disposition: FFprobeStreamDisposition, + + /** + * The stream's tags + */ + tags?: FFprobeStreamTags +} + +/** + * The "tags" field on an FFprobe response chapter object + */ +export interface FFprobeChapterTags { + /** + * The chapter title + */ + title: string +} + +/** + * An FFprobe response chapter object + */ +export interface FFprobeChapter { + /** + * The chapter ID + */ + id: number, + + /** + * The division equation to use for converting integer representations of timestamps into seconds (e.g. "1/30000" turns 80632552 into 2687.751733 seconds) + */ + time_base: string, + + /** + * When the chapter starts in integer timestamp format (defined by time_base) + */ + start: number, + + /** + * The string representation of a floating point integer showing when the chapter starts in seconds + */ + start_time: string, + + /** + * When the chapter end in integer timestamp format (defined by time_base) + */ + end: number, + + /** + * The string representation of a floating point integer showing when the chapter ends in seconds + */ + end_time: string, + + /** + * The chapter's tags + */ + tags: FFprobeChapterTags +} + +/** + * The "tags" field on an FFprobe response format object + */ +export interface FFprobeFormatTags { + /** + * Not clear, probably the media type brand, but not sure + */ + major_brand: string, + + /** + * The brand version perhaps, but not sure + */ + minor_version: string, + + /** + * The brands that are compatible with the referenced brands perhaps, but not sure + */ + compatible_brands: string, + + /** + * The media's title (song metadata uses an all uppercase version) + */ + title?: string, + + /** + * The media artist (song metadata uses an all uppercase version) + */ + artist?: string, + + /** + * The media's creation date, seems to be in YYYYMMDD format (song metadata uses an all uppercase version) + */ + date?: string, + + /** + * The name of the encoder responsible for encoding the media + */ + encoder?: string, + + /** + * The comment attached to the file + */ + comment?: string, + + /** + * The description attached to the file + */ + description?: string, + + /** + * The ISO-formatted date (although it may use other formats) when the media was created + */ + creation_time?: string, + + /** + * The album (only present in audio files) + */ + ALBUM?: string, + + /** + * The album arist (only present in audio files) + */ + album_artist?: string, + + /** + * The album artist name used for sorting probably (only present in audio files) + */ + ALBUMARTISTSORT?: string, + + /** + * The song artist (only present in audio files) + */ + ARTIST?: string, + + /** + * The date when the song was created (no particular format, often the year) (only present in audio files) + */ + DATE?: string, + + /** + * The string representation of an integer showing the song's disc number (only present in audio files) + */ + disc?: string, + + /** + * The string representation of an integer showing the total number of discs comprising the album the song is in (only present in audio files) + */ + DISCTOTAL?: string, + + /** + * The song's International Standard Recording Code + */ + ISRC?: string, + + /** + * The song's genre (only present in audio files) + */ + GENRE?: string, + + /** + * The song's title (only present in audio files) + */ + TITLE?: string, + + /** + * The string representation of an integer showing the song's track number (only present in audio files) + */ + track?: string, + + /** + * The string representation of an integer showing the total number of tracks in the album the song is in (only present in audio files) + */ + TRACKTOTAL?: string, + + /** + * The string representation of an integer showing the year the song was created (only present in audio files) + */ + YEAR?: string, + + /** + * The string representation of an integer showing the song's BPM (only present in audio files) + */ + BPM?: string, + + /** + * The song's publisher (only present in audio files) + */ + PUBLISHER?: string +} + +/** + * An FFprobe response format object + */ +export interface FFprobeFormat { + /** + * The path of the probed file (as specified in the input file argument) + */ + filename: string, + + /** + * The total number of streams present + */ + nb_streams: number, + + /** + * The total number of programs present + */ + nb_programs: number, + + /** + * The name of the format (a comma separated list of applicable file extensions for the format) + */ + format_name: string, + + /** + * The long (detailed) name of the format + */ + format_long_name: string, + + /** + * The string representation of a floating point integer showing the file's starting time + */ + start_time: string, + + /** + * The string representation of a floating point integer showing the file's duration in seconds (seems to be a non-accurate, rounded version of the real duration) + */ + duration: string, + + /** + * The string representation of a long integer showing the file's size in bytes + */ + size: string, + + /** + * The string representation of a long integer showing the file's stated bitrate (may vary between streams, probably applies to just video if a video file) + */ + bit_rate: string, + + /** + * A score of how confident FFprobe is of the format, 0 to 100. https://stackoverflow.com/questions/25257986/what-does-probe-score-mean-in-ffprobe-output + */ + probe_score: number, + + /** + * The format's tags + */ + tags?: FFprobeFormatTags +} + +/** + * An FFprobe error object + */ +export interface FFprobeProbeError { + /** + * The error code + */ + code: number, + + /** + * The error message + */ + string: string +} + +/** + * An FFprobe probe result object + */ +export interface FFprobeProbeResult { + /** + * The probed file's streams (-show_streams flag required) + */ + streams?: FFprobeStream[], + + /** + * The probed file's chapters (-show_chapters flag required) + */ + chapters?: FFprobeChapter[], + + /** + * The probed file's format data (-show_format flag required) + */ + format?: FFprobeFormat, + + /** + * The error that occurred when trying to probe the file (-show_error flag required) + */ + error?: FFprobeProbeError +} diff --git a/public/ffmpeg.js b/public/ffmpeg.js index c35c677b..fc1ae084 100644 --- a/public/ffmpeg.js +++ b/public/ffmpeg.js @@ -152,6 +152,7 @@ async function runFfprobe(args, { timeout = isDev ? 10000 : 30000 } = {}) { } } +/** @type {(a: any) => Promise} */ async function renderWaveformPng({ filePath, start, duration, color, streamIndex }) { const args1 = [ '-hide_banner', diff --git a/src/App.tsx b/src/App.tsx index 2bd55eb4..e71b404e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,7 +29,7 @@ import useFrameCapture from './hooks/useFrameCapture'; import useSegments from './hooks/useSegments'; import useDirectoryAccess, { DirectoryAccessDeclinedError } from './hooks/useDirectoryAccess'; -import { UserSettingsContext, SegColorsContext } from './contexts'; +import { UserSettingsContext, SegColorsContext, UserSettingsContextType } from './contexts'; import NoFileLoaded from './NoFileLoaded'; import MediaSourcePlayer from './MediaSourcePlayer'; @@ -86,8 +86,9 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti import BigWaveform from './components/BigWaveform'; import isDev from './isDev'; -import { EdlFileType, FfmpegCommandLog, FfprobeChapter, FfprobeFormat, FfprobeStream, FormatTimecode, Html5ifyMode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types'; +import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, Html5ifyMode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types'; import { CaptureFormat, KeyboardAction } from '../types'; +import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../ffprobe'; const electron = window.require('electron'); const { exists } = window.require('fs-extra'); @@ -124,11 +125,11 @@ function App() { const [cutProgress, setCutProgress] = useState(); const [startTimeOffset, setStartTimeOffset] = useState(0); const [filePath, setFilePath] = useState(); - const [externalFilesMeta, setExternalFilesMeta] = useState({}); + const [externalFilesMeta, setExternalFilesMeta] = useState>({}); const [customTagsByFile, setCustomTagsByFile] = useState({}); const [paramsByStreamId, setParamsByStreamId] = useState(new Map()); const [detectedFps, setDetectedFps] = useState(); - const [mainFileMeta, setMainFileMeta] = useState<{ streams: FfprobeStream[], formatData: FfprobeFormat, chapters?: FfprobeChapter[] }>({ streams: [], formatData: {} }); + const [mainFileMeta, setMainFileMeta] = useState<{ streams: FFprobeStream[], formatData: FFprobeFormat, chapters: FFprobeChapter[] }>(); const [copyStreamIdsByFile, setCopyStreamIdsByFile] = useState>>({}); const [streamsSelectorShown, setStreamsSelectorShown] = useState(false); const [concatDialogVisible, setConcatDialogVisible] = useState(false); @@ -149,7 +150,7 @@ function App() { const { fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, isCustomFormatSelected } = useFileFormatState(); // State per application launch - const lastOpenedPathRef = useRef(); + const lastOpenedPathRef = useRef(); const [waveformMode, setWaveformMode] = useState<'big-waveform' | 'waveform'>(); const [thumbnailsEnabled, setThumbnailsEnabled] = useState(false); const [keyframesEnabled, setKeyframesEnabled] = useState(true); @@ -209,7 +210,7 @@ function App() { electron.ipcRenderer.send('setLanguage', l); }, [language]); - const videoRef = useRef(null); + const videoRef = useRef(null); const videoContainerRef = useRef(null); const setOutputPlaybackRate = useCallback((v) => { @@ -304,15 +305,15 @@ function App() { commandedTimeRef.current = commandedTime; }, [commandedTime]); - const mainStreams = useMemo(() => mainFileMeta.streams, [mainFileMeta.streams]); - const mainFileFormatData = useMemo(() => mainFileMeta.formatData, [mainFileMeta.formatData]); - const mainFileChapters = useMemo(() => mainFileMeta.chapters, [mainFileMeta.chapters]); + const mainStreams = useMemo(() => mainFileMeta?.streams ?? [], [mainFileMeta?.streams]); + const mainFileFormatData = useMemo(() => mainFileMeta?.formatData, [mainFileMeta?.formatData]); + const mainFileChapters = useMemo(() => mainFileMeta?.chapters, [mainFileMeta?.chapters]); const isCopyingStreamId = useCallback((path, streamId) => ( !!(copyStreamIdsByFile[path] || {})[streamId] ), [copyStreamIdsByFile]); - const checkCopyingAnyTrackOfType = useCallback((filter) => mainStreams.some((stream) => isCopyingStreamId(filePath, stream.index) && filter(stream)), [filePath, isCopyingStreamId, mainStreams]); + const checkCopyingAnyTrackOfType = useCallback((filter: (s: FFprobeStream) => boolean) => mainStreams.some((stream) => isCopyingStreamId(filePath, stream.index) && filter(stream)), [filePath, isCopyingStreamId, mainStreams]); const copyAnyAudioTrack = useMemo(() => checkCopyingAnyTrackOfType((stream) => stream.codec_type === 'audio'), [checkCopyingAnyTrackOfType]); const subtitleStreams = useMemo(() => getSubtitleStreams(mainStreams), [mainStreams]); @@ -328,7 +329,7 @@ function App() { // 360 means we don't modify rotation gtrgt const isRotationSet = rotation !== 360; - const effectiveRotation = useMemo(() => (isRotationSet ? rotation : (activeVideoStream?.tags?.rotate && parseInt(activeVideoStream.tags.rotate, 10))), [isRotationSet, activeVideoStream, rotation]); + const effectiveRotation = useMemo(() => (isRotationSet ? rotation : (activeVideoStream?.tags?.rotate ? parseInt(activeVideoStream.tags.rotate, 10) : undefined)), [isRotationSet, activeVideoStream, rotation]); const zoomRel = useCallback((rel) => setZoom((z) => Math.min(Math.max(z + (rel * (1 + (z / 10))), 1), zoomMax)), []); const compatPlayerRequired = usingDummyVideo; @@ -568,7 +569,7 @@ function App() { }), [hideAllNotifications, setInvertCutSegments, setSimpleMode]); const effectiveExportMode = useMemo(() => { - if (segmentsToChaptersOnly) return 'sesgments_to_chapters'; + if (segmentsToChaptersOnly) return 'segments_to_chapters'; if (autoMerge && autoDeleteMergedSegments) return 'merge'; if (autoMerge) return 'merge+separate'; return 'separate'; @@ -603,7 +604,7 @@ function App() { setStoreProjectInWorkingDir(newValue); }, [ensureAccessToSourceDir, getProjectFileSavePath, setStoreProjectInWorkingDir, storeProjectInWorkingDir]); - const userSettingsContext = useMemo(() => ({ + const userSettingsContext = useMemo(() => ({ ...allUserSettings, toggleCaptureFormat, changeOutDir, toggleKeyframeCut, togglePreserveMovData, toggleMovFastStart, toggleExportConfirmEnabled, toggleSegmentsToChapters, togglePreserveMetadataOnMerge, toggleSimpleMode, toggleSafeOutputFileName, effectiveExportMode, }), [allUserSettings, changeOutDir, effectiveExportMode, toggleCaptureFormat, toggleExportConfirmEnabled, toggleKeyframeCut, toggleMovFastStart, togglePreserveMetadataOnMerge, togglePreserveMovData, toggleSafeOutputFileName, toggleSegmentsToChapters, toggleSimpleMode]); @@ -671,7 +672,7 @@ function App() { const allFilesMeta = useMemo(() => ({ ...externalFilesMeta, - ...(filePath ? { [filePath]: mainFileMeta } : {}), + ...(filePath && mainFileMeta != null ? { [filePath]: mainFileMeta } : {}), }), [externalFilesMeta, filePath, mainFileMeta]); // total number of streams for ALL files @@ -746,7 +747,7 @@ function App() { const shouldShowWaveform = calcShouldShowWaveform(zoomedDuration); const { neighbouringKeyFrames, findNearestKeyFrameTime } = useKeyframes({ keyframesEnabled, filePath, commandedTime, videoStream: activeVideoStream, detectedFps, ffmpegExtractWindow }); - const { waveforms } = useWaveform({ darkMode, filePath, relevantTime, waveformEnabled, audioStream: activeAudioStream, shouldShowWaveform, ffmpegExtractWindow, durationSafe }); + const { waveforms } = useWaveform({ darkMode, filePath, relevantTime, waveformEnabled, audioStream: activeAudioStream, ffmpegExtractWindow, durationSafe }); const resetMergedOutFileName = useCallback(() => { if (fileFormat == null || filePath == null) return; @@ -784,7 +785,7 @@ function App() { setCustomTagsByFile({}); setParamsByStreamId(new Map()); setDetectedFps(undefined); - setMainFileMeta({ streams: [], formatData: [] }); + setMainFileMeta(undefined); setCopyStreamIdsByFile({}); setStreamsSelectorShown(false); setZoom(1); @@ -1298,6 +1299,7 @@ function App() { if (!exportConfirmEnabled) notices.push(i18n.t('Export options are not shown. You can enable export options by clicking the icon right next to the export button.')); + invariant(mainFileFormatData != null); // https://github.com/mifi/lossless-cut/issues/329 if (isIphoneHevc(mainFileFormatData, mainStreams)) warnings.push(i18n.t('There is a known issue with cutting iPhone HEVC videos. The output file may not work in all players.')); @@ -1648,7 +1650,7 @@ function App() { }, [ensureAccessToSourceDir, loadMedia]); // todo merge with userOpenFiles? - const batchOpenSingleFile = useCallback(async (path) => { + const batchOpenSingleFile = useCallback(async (path: string) => { if (workingRef.current) return; if (filePath === path) return; try { @@ -1664,7 +1666,7 @@ function App() { const batchFileJump = useCallback((direction: number, alsoOpen: boolean) => { if (batchFiles.length === 0) return; - let newSelectedBatchFiles: string[]; + let newSelectedBatchFiles: [string]; if (selectedBatchFiles.length === 0) { newSelectedBatchFiles = [batchFiles[0]!.path]; } else { @@ -1681,8 +1683,9 @@ function App() { }, [batchFiles, batchOpenSingleFile, selectedBatchFiles]); const batchOpenSelectedFile = useCallback(() => { - if (selectedBatchFiles.length === 0) return; - batchOpenSingleFile(selectedBatchFiles[0]); + const [firstSelectedBatchFile] = selectedBatchFiles; + if (firstSelectedBatchFile == null) return; + batchOpenSingleFile(firstSelectedBatchFile); }, [batchOpenSingleFile, selectedBatchFiles]); const onBatchFileSelect = useCallback((path: string) => { @@ -1814,10 +1817,10 @@ function App() { setter(fileMap.get(streamId)); })), [setParamsByStreamId]); - const addFileAsCoverArt = useCallback(async (path) => { + const addFileAsCoverArt = useCallback(async (path: string) => { const fileMeta = await addStreamSourceFile(path); if (!fileMeta) return false; - const firstIndex = fileMeta.streams[0].index; + const firstIndex = fileMeta.streams[0]!.index; updateStreamParams(path, firstIndex, (params) => params.set('disposition', 'attached_pic')); return true; }, [addStreamSourceFile, updateStreamParams]); @@ -1852,14 +1855,14 @@ function App() { }); }, []); - const userOpenFiles = useCallback(async (filePathsIn) => { + const userOpenFiles = useCallback(async (filePathsIn?: string[]) => { let filePaths = filePathsIn; if (!filePaths || filePaths.length === 0) return; console.log('userOpenFiles'); console.log(filePaths.join('\n')); - [lastOpenedPathRef.current] = filePaths; + lastOpenedPathRef.current = filePaths[0]!; // first check if it is a single directory, and if so, read it recursively if (filePaths.length === 1) { @@ -1895,7 +1898,8 @@ function App() { } // filePaths.length is now 1 - const firstFilePath = filePaths[0]; + const [firstFilePath] = filePaths; + invariant(firstFilePath != null); // https://en.wikibooks.org/wiki/Inside_DVD-Video/Directory_Structure if (/^video_ts$/i.test(basename(firstFilePath))) { @@ -2461,7 +2465,6 @@ function App() {
- {compatPlayerEnabled && } + {filePath != null && compatPlayerEnabled && }
{bigWaveformEnabled && } @@ -2698,8 +2701,7 @@ function App() { /> - {/* @ts-expect-error todo */} - + setStreamsSelectorShown(false)} maxWidth={1000}> {mainStreams && ( diff --git a/src/MediaSourcePlayer.tsx b/src/MediaSourcePlayer.tsx index 3d37952f..8f8ac3ec 100644 --- a/src/MediaSourcePlayer.tsx +++ b/src/MediaSourcePlayer.tsx @@ -1,8 +1,10 @@ -import { useEffect, useRef, useState, useCallback, useMemo, memo, CSSProperties } from 'react'; +import { useEffect, useRef, useState, useCallback, useMemo, memo, CSSProperties, RefObject } from 'react'; import { Spinner } from 'evergreen-ui'; import { useDebounce } from 'use-debounce'; import isDev from './isDev'; +import { ChromiumHTMLVideoElement } from './types'; +import { FFprobeStream } from '../ffprobe'; const remote = window.require('@electron/remote'); const { createMediaSourceStream, readOneJpegFrame } = remote.require('./compatPlayer'); @@ -10,7 +12,7 @@ const { createMediaSourceStream, readOneJpegFrame } = remote.require('./compatPl async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex, seekTo, signal, playSafe, onCanPlay, getTargetTime, size, fps }: { path: string, - video: HTMLVideoElement, + video: ChromiumHTMLVideoElement, videoStreamIndex?: number | undefined, audioStreamIndex?: number | undefined, seekTo: number, @@ -253,7 +255,9 @@ async function createPauseImage({ path, seekTo, videoStreamIndex, canvas, signal drawJpegFrame(canvas, jpegImage); } -function MediaSourcePlayer({ rotate, filePath, playerTime, videoStream, audioStream, commandedTime, playing, eventId, masterVideoRef, mediaSourceQuality, playbackVolume }) { +function MediaSourcePlayer({ rotate, filePath, playerTime, videoStream, audioStream, commandedTime, playing, eventId, masterVideoRef, mediaSourceQuality, playbackVolume }: { + rotate: number | undefined, filePath: string, playerTime: number, videoStream: FFprobeStream | undefined, audioStream: FFprobeStream | undefined, commandedTime: number, playing: boolean, eventId: number, masterVideoRef: RefObject, mediaSourceQuality: number, playbackVolume: number, +}) { const videoRef = useRef(null); const canvasRef = useRef(null); const [loading, setLoading] = useState(true); @@ -292,7 +296,7 @@ function MediaSourcePlayer({ rotate, filePath, playerTime, videoStream, audioStr } const onCanPlay = () => setLoading(false); - const getTargetTime = () => masterVideoRef.current.currentTime - debouncedState.startTime; + const getTargetTime = () => masterVideoRef.current!.currentTime - debouncedState.startTime; const abortController = new AbortController(); diff --git a/src/TopMenu.jsx b/src/TopMenu.tsx similarity index 74% rename from src/TopMenu.jsx rename to src/TopMenu.tsx index 00063c6f..2c3a9bf2 100644 --- a/src/TopMenu.jsx +++ b/src/TopMenu.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback } from 'react'; +import { CSSProperties, ReactNode, memo, useCallback } from 'react'; import { IoIosSettings } from 'react-icons/io'; import { FaLock, FaUnlock } from 'react-icons/fa'; import { CrossIcon, ListIcon, VolumeUpIcon, VolumeOffIcon } from 'evergreen-ui'; @@ -16,9 +16,31 @@ const outFmtStyle = { height: 20, maxWidth: 100 }; const exportModeStyle = { flexGrow: 0, flexBasis: 140 }; const TopMenu = memo(({ - filePath, fileFormat, copyAnyAudioTrack, toggleStripAudio, - renderOutFmt, numStreamsToCopy, numStreamsTotal, setStreamsSelectorShown, toggleSettings, - selectedSegments, isCustomFormatSelected, clearOutDir, + filePath, + fileFormat, + copyAnyAudioTrack, + toggleStripAudio, + renderOutFmt, + numStreamsToCopy, + numStreamsTotal, + setStreamsSelectorShown, + toggleSettings, + selectedSegments, + isCustomFormatSelected, + clearOutDir, +}: { + filePath: string | undefined, + fileFormat: string | undefined, + copyAnyAudioTrack: boolean, + toggleStripAudio: () => void, + renderOutFmt: (style: CSSProperties) => ReactNode, + numStreamsToCopy: number, + numStreamsTotal: number, + setStreamsSelectorShown: (v: boolean) => void, + toggleSettings: () => void, + selectedSegments, + isCustomFormatSelected, + clearOutDir, }) => { const { t } = useTranslation(); const { customOutDir, changeOutDir, simpleMode, outFormatLocked, setOutFormatLocked } = useUserSettings(); @@ -40,7 +62,7 @@ const TopMenu = memo(({ {filePath && ( <> @@ -49,9 +71,9 @@ const TopMenu = memo(({ onClick={withBlur(toggleStripAudio)} > {copyAnyAudioTrack ? ( - <>{t('Keep audio')} + <>{t('Keep audio')} ) : ( - <>{t('Discard audio')} + <>{t('Discard audio')} )} diff --git a/src/components/BigWaveform.tsx b/src/components/BigWaveform.tsx index 656decc9..9b68025e 100644 --- a/src/components/BigWaveform.tsx +++ b/src/components/BigWaveform.tsx @@ -1,10 +1,10 @@ import { memo, useEffect, useState, useCallback, useRef } from 'react'; import { ffmpegExtractWindow } from '../util/constants'; -import { Waveform } from '../types'; +import { RenderableWaveform } from '../types'; const BigWaveform = memo(({ waveforms, relevantTime, playing, durationSafe, zoom, seekRel }: { - waveforms: Waveform[], relevantTime: number, playing: boolean, durationSafe: number, zoom: number, seekRel: (a: number) => void, + waveforms: RenderableWaveform[], relevantTime: number, playing: boolean, durationSafe: number, zoom: number, seekRel: (a: number) => void, }) => { const windowSize = ffmpegExtractWindow * 2; const windowStart = Math.max(0, relevantTime - windowSize); diff --git a/src/components/Button.jsx b/src/components/Button.jsx deleted file mode 100644 index 74536186..00000000 --- a/src/components/Button.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { memo } from 'react'; - -import styles from './Button.module.css'; - -const Button = memo(({ type = 'button', ...props }) => ( - // eslint-disable-next-line react/jsx-props-no-spreading, react/button-has-type -