-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 9c435d1
Showing
10 changed files
with
603 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.vscode | ||
dev | ||
ddmpeg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2021 deanveloper | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# ddmpeg | ||
|
||
a personal project to make an ffmpeg wrapper that trims videos, sets target file size | ||
|
||
## usage | ||
|
||
`ddmpeg -i <input> -o <output> [-r <range>] [-s <size>] [-m <1[,...]>] [-d]` | ||
|
||
| flag | description | usage | | ||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------- | ---------------- | | ||
| `input` or `i` | specify input file | `-i infile.mp4` | | ||
| `output` or `o` | specify output file | `-o outfile.mp4` | | ||
| `trim` or `t` | specify trim range. must contain a dash, remove one argument to specify the start/end of the video | `-t 2m40s:3m` | | ||
| `size` or `s` | specify target size of video | `-s 4m` (4 MB) | | ||
| `merge` or `m` | merge audio tracks into one track, can optionally specify weights for each track. All unlisted tracks assume `1` as the weight. | `-m` or `-m 2,3` | | ||
|
||
## examples | ||
|
||
#### trim a video | ||
|
||
- Trim video to only include 3min to 3min 30sec | ||
- `ddmpeg -i in.mp4 -o out.mp4 -t 3m:3m30s` | ||
- Trim video to include everything after 4mins 34.3secs | ||
- `ddmpeg -i in.mp4 -o out.mp4 -t 4m34.3s:` | ||
- Trim video to only include the first 30 seconds | ||
- `ddmpeg -i in.mp4 -o out.mp4 -t :30s` | ||
|
||
#### audio stuff | ||
|
||
- Merge audio tracks with equal weights | ||
- `ddmpeg -i in.mp4 -o out.mp4 -m 1` | ||
- Merge audio tracks, with the second track being louder than the first | ||
- `ddmpeg -i in.mp4 -o out.mp4 -m 1,5` | ||
|
||
#### set target size | ||
|
||
- Try to shoot for a 30MB video | ||
- `ddmpeg -i in.mp4 -o out.mp4 -s 30m` | ||
|
||
#### putting it all together | ||
|
||
- Share your Valorant clip! | ||
- Your clipping software captures 5 minutes, but you only want the last 30 seconds. | ||
- The first audio stream has your desktop sounds, and the second audio stream has your microphone sounds. | ||
- Your microphone was pretty loud, so you want to make sure that has less weight when we mix the streams. | ||
- You're going to share this on Discord, so you can't let it go over 100MB | ||
- `ddmpeg -i in.mp4 -o out.mp4 -t 4m30s: -m 3,1 -s 75m` | ||
|
||
## installation (from source) | ||
|
||
1. install [deno](https://deno.land/#installation) | ||
2. clone this repo | ||
1. https: `git clone https://github.com/deanveloper/ddmpeg` | ||
2. ssh: `[email protected]:deanveloper/ddmpeg.git` | ||
3. `deno compile --allow-read --allow-run main.ts` | ||
4. `cp` the newly created binary somewhere in your PATH (ie `/usr/local/bin` on *nix) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"fmt": { | ||
"options": { | ||
"useTabs": true, | ||
"lineWidth": 120 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export class FFTrimError extends Error { | ||
constructor(msg: string) { | ||
super(msg); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
import { readStringDelim } from "https://deno.land/std/io/mod.ts"; | ||
import { readAll } from "https://deno.land/std/streams/mod.ts"; | ||
import { clockToSeconds } from "./timeFormats.ts"; | ||
import { Weights } from "./weights.ts"; | ||
|
||
export type VideoData = { | ||
durationSeconds: number; | ||
streams: Stream[]; | ||
}; | ||
export type Stream = { | ||
index: number; | ||
type: "video" | "audio"; | ||
codec: string; | ||
}; | ||
|
||
export type TrimArgs = { | ||
start: number | undefined; | ||
end: number | undefined; | ||
bitrate: number | undefined; | ||
inputFile: string; | ||
outputFile: string; | ||
audioWeights?: Weights; | ||
copyAudio: boolean; | ||
}; | ||
|
||
export async function getVideoData(file: string): Promise<VideoData> { | ||
// deno-fmt-ignore | ||
const p = Deno.run({ | ||
cmd: [ | ||
"ffprobe", | ||
file, | ||
"-v", "error", | ||
"-print_format", "json", | ||
"-show_streams", "-show_format" | ||
], | ||
stdout: "piped", | ||
stderr: "null", | ||
}); | ||
|
||
const output = JSON.parse(new TextDecoder().decode(await readAll(p.stdout))); | ||
const videoData = { | ||
durationSeconds: Number(output.format.duration), | ||
streams: output.streams.map((stream: any) => ({ | ||
index: stream.index, | ||
type: stream.codec_type, | ||
codec: stream.codec_name, | ||
})), | ||
}; | ||
if (!validateVideoData(videoData)) { | ||
throw new Error(`invalid video data: ${JSON.stringify(videoData)}`); | ||
} | ||
return videoData; | ||
} | ||
|
||
export async function* trim(options: TrimArgs): AsyncGenerator<number> { | ||
const { | ||
start, | ||
end, | ||
bitrate, | ||
inputFile, | ||
audioWeights, | ||
outputFile, | ||
copyAudio, | ||
} = options; | ||
|
||
const startArgs = start ? ["-ss", start.toString(10)] : []; | ||
const endArgs = end ? ["-to", end.toString(10)] : []; | ||
const bitrateArgs = bitrate ? ["-b:v", bitrate.toString(10)] : []; | ||
const audioWeightsArgs = audioWeights ? parseAudioWeightsArgs(audioWeights) : []; | ||
const copyAudioArgs = copyAudio ? ["-c:a", "copy"] : []; | ||
|
||
const cmd = [ | ||
"ffmpeg", | ||
|
||
"-i", | ||
inputFile, | ||
...startArgs, | ||
...endArgs, | ||
...bitrateArgs, | ||
...audioWeightsArgs, | ||
...copyAudioArgs, | ||
"-loglevel", | ||
"level+info", | ||
"-y", | ||
outputFile, | ||
]; | ||
|
||
console.log(cmd.map((s) => `"${s}"`).join(" ")); | ||
|
||
const p = Deno.run({ | ||
cmd, | ||
stdout: "null", | ||
stderr: "piped", | ||
}); | ||
|
||
yield* readProgressUpdates(p.stderr); | ||
} | ||
|
||
// returns an async generator that yields the number of seconds that the encoder has currently encoded. | ||
async function* readProgressUpdates( | ||
stderr: Deno.Reader, | ||
): AsyncGenerator<number> { | ||
const progressRegex = /time=([0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{2,}))/; | ||
|
||
let startedReading = false; | ||
for await (const progressUpdateLine of readStringDelim(stderr, "\r")) { | ||
const groups = progressRegex.exec(progressUpdateLine); | ||
const result = groups?.[1]; | ||
|
||
if (startedReading && !result) { | ||
break; | ||
} | ||
if (result) { | ||
startedReading = true; | ||
yield clockToSeconds(result); | ||
} | ||
} | ||
} | ||
|
||
// audioTracks: | ||
// represents which weights go to which tracks | ||
function parseAudioWeightsArgs(audioWeights: Weights): string[] { | ||
// if empty, we remove all audio tracks | ||
if (audioWeights.type === "none") { | ||
return [ | ||
"-map", | ||
"0", | ||
"-map", | ||
"0:a", | ||
]; | ||
} | ||
|
||
if (audioWeights.type === "single") { | ||
return [ | ||
"-map", | ||
"0:v:0", | ||
"-map", | ||
`0:a:${audioWeights.weights.keys().next().value}`, | ||
]; | ||
} | ||
|
||
if (audioWeights.type === "multi") { | ||
// deno-fmt-ignore | ||
return [ | ||
"-filter_complex", amix(audioWeights.weights, false), | ||
"-map", "0:V:0", | ||
"-map", "[aout]", | ||
]; | ||
} | ||
|
||
// if (audioWeights.type === 'weighted') ... | ||
// deno-fmt-ignore | ||
return [ | ||
"-filter_complex", amix(audioWeights.weights, true), | ||
"-map", "0:V:0", | ||
"-map", "[aout]", | ||
]; | ||
} | ||
|
||
// takes weights, returns amix filter | ||
function amix(weights: Map<string, number>, useWeights: boolean): string { | ||
let amix = ""; | ||
for (const track of weights.keys()) { | ||
amix += `[0:a:${track}]`; | ||
} | ||
|
||
amix += `amix=inputs=${weights.size}:duration=longest`; | ||
|
||
if (!useWeights) { | ||
amix += `:weights=${[...weights.values()].join(" ")}`; | ||
} | ||
amix += "[aout]"; | ||
|
||
return amix; | ||
} | ||
|
||
function validateVideoData(data: any): data is VideoData { | ||
if (!data || typeof data !== "object") { | ||
return false; | ||
} | ||
|
||
if (typeof data.durationSeconds !== "number") { | ||
return false; | ||
} | ||
if (isNaN(data.durationSeconds)) { | ||
return false; | ||
} | ||
|
||
for (const stream of data?.streams) { | ||
if (!stream || typeof stream !== "object") { | ||
return false; | ||
} | ||
if (typeof stream.index !== "number") { | ||
return false; | ||
} | ||
if (stream.type !== "video" && stream.type !== "audio") { | ||
return false; | ||
} | ||
if (typeof stream.codec !== "string") { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { Flags, parseFlags } from "./parseFlags.ts"; | ||
import { getVideoData, trim } from "./ffwrapper.ts"; | ||
import { FFTrimError } from "./error.ts"; | ||
import { parseWeightsArray } from "./weights.ts"; | ||
|
||
let flags: Flags; | ||
try { | ||
flags = parseFlags(); | ||
} catch (err) { | ||
if (err instanceof FFTrimError) { | ||
console.log( | ||
`usage: ddmpeg -i <input> -o <output> [-t <[start]:[end]>] [-s <size>] [-m <w1>[,w2,w3...]]`, | ||
); | ||
console.error("caused by: " + err); | ||
Deno.exit(1); | ||
} else { | ||
throw err; | ||
} | ||
} | ||
let { | ||
start, | ||
end, | ||
size, | ||
inputFile, | ||
outputFile, | ||
mergeWeights, | ||
} = flags; | ||
|
||
const videoData = await getVideoData(inputFile); | ||
const audioStreams = videoData.streams.filter((stream) => stream.type === "audio"); | ||
|
||
// set defaults for start/end | ||
if (start === undefined) { | ||
start = 0; | ||
} | ||
if (end === undefined) { | ||
end = videoData.durationSeconds; | ||
} | ||
|
||
// parse audio weights | ||
if (mergeWeights) { | ||
for (let i = mergeWeights.length; i < audioStreams.length; i++) { | ||
mergeWeights[i] = 1; | ||
} | ||
} | ||
const audioWeights = parseWeightsArray(mergeWeights); | ||
|
||
const duration = end - start; | ||
|
||
const copyAudio = audioWeights.canCopyAudio; | ||
|
||
// trim the video (async) | ||
const progress = trim({ | ||
start, | ||
end, | ||
bitrate: size ? size / duration : undefined, | ||
inputFile, | ||
outputFile, | ||
audioWeights, | ||
copyAudio, | ||
}); | ||
|
||
// print out progress on the trimming | ||
const encoder = new TextEncoder(); | ||
for await (const secondsDone of progress) { | ||
const percent = secondsDone / duration; | ||
Deno.stdout.writeSync( | ||
encoder.encode( | ||
`\r${progressBar(30, percent)} (${(percent * 100).toFixed(1)}%)`, | ||
), | ||
); | ||
} | ||
|
||
// write a newline | ||
Deno.stdout.writeSync(new Uint8Array([10])); | ||
|
||
// ======== UTILITY FUNCTIONS ========= | ||
|
||
// simple progress bar | ||
function progressBar(width: number, percent: number) { | ||
percent = Math.max(0, Math.min(percent, 1)); | ||
|
||
const equalsSigns = Math.floor((width - 2) * percent); | ||
const spaces = Math.ceil((width - 2) * (1 - percent)); | ||
|
||
return `[${"=".repeat(equalsSigns)}${" ".repeat(spaces)}]`; | ||
} |
Oops, something went wrong.