diff --git a/Cargo.lock b/Cargo.lock index c891b51..4b3eee7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -873,6 +873,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "jni" version = "0.21.1" @@ -1049,6 +1055,20 @@ dependencies = [ "paste", ] +[[package]] +name = "mp4" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9ef834d5ed55e494a2ae350220314dc4aacd1c43a9498b00e320e0ea352a5c3" +dependencies = [ + "byteorder", + "bytes", + "num-rational", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "mp4ra-rust" version = "0.3.0" @@ -1120,6 +1140,37 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1741,6 +1792,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2121,6 +2185,7 @@ version = "0.1.0" dependencies = [ "clap", "glam", + "mp4", "strafesnet_common", "strafesnet_graphics", "strafesnet_roblox_bot_file", @@ -2952,3 +3017,9 @@ dependencies = [ "quote", "syn 2.0.117", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/video-encoder/Cargo.toml b/video-encoder/Cargo.toml index 6e3afc0..4a00eca 100644 --- a/video-encoder/Cargo.toml +++ b/video-encoder/Cargo.toml @@ -13,3 +13,4 @@ strafesnet_roblox_bot_file.workspace = true strafesnet_snf.workspace = true vk-video = "0.2.0" clap = { version = "4.5.60", features = ["derive"] } +mp4 = "0.14.0" diff --git a/video-encoder/src/encode.rs b/video-encoder/src/encode.rs index 94526d2..44b2c65 100644 --- a/video-encoder/src/encode.rs +++ b/video-encoder/src/encode.rs @@ -1,4 +1,3 @@ -use std::io::Write; use std::num::NonZeroU32; use std::path::PathBuf; use strafesnet_common::session::Time as SessionTime; @@ -48,7 +47,7 @@ impl EncodeSubcommand{ device:self.device, output_file:self.output_file.unwrap_or_else(||{ let mut output_file:PathBuf=self.bot.file_stem().unwrap().into(); - output_file.set_extension("h264"); + output_file.set_extension("mp4"); output_file }), map:self.map, @@ -73,7 +72,10 @@ enum EncodeError{ VideoCreateTextures(vk_video::VulkanEncoderError), VideoEncodeFrame(vk_video::VulkanEncoderError), OutputCreateFile(std::io::Error), - OutputWriteFile(std::io::Error), + OutputMp4Start(mp4::Error), + OutputMp4AddTrack(mp4::Error), + OutputMp4WriteSample(mp4::Error), + OutputMp4End(mp4::Error), } struct EncodeParams{ @@ -154,9 +156,41 @@ fn encode(params:EncodeParams)->Result<(),EncodeError>{ ) .map_err(EncodeError::VideoCreateTextures)?; - let mut output_file = std::fs::File::create(params.output_file) + let output_file=std::fs::File::create(params.output_file) .map_err(EncodeError::OutputCreateFile)?; + let mp4_config=mp4::Mp4Config{ + major_brand: str::parse("isom").unwrap(), + minor_version: 512, + compatible_brands: vec![ + str::parse("isom").unwrap(), + str::parse("iso2").unwrap(), + str::parse("avc1").unwrap(), + str::parse("mp41").unwrap(), + ], + timescale:target_framerate, + }; + let mut mp4=mp4::Mp4Writer::write_start(output_file,&mp4_config) + .map_err(EncodeError::OutputMp4Start)?; + + let avc_config=mp4::AvcConfig{ + width:params.width.get() as u16, + height:params.height.get() as u16, + // make up some data to prevent this underdeveloped library from crashing + seq_param_set:vec![0,0,0,0], + pic_param_set:vec![], + }; + let track_config=mp4::TrackConfig{ + track_type:mp4::TrackType::Video, + timescale:target_framerate, + language:"eng".to_owned(), + media_conf:mp4::MediaConfig::AvcConfig(avc_config), + }; + + const TRACK_ID:u32=1; + mp4.add_track(&track_config) + .map_err(EncodeError::OutputMp4AddTrack)?; + let duration = bot.duration(); for i in 0..duration.get()*target_framerate as i64/SessionTime::ONE_SECOND.get() { let time=SessionTime::raw(i*SessionTime::ONE_SECOND.get()/target_framerate as i64); @@ -171,10 +205,20 @@ fn encode(params:EncodeParams)->Result<(),EncodeError>{ let res=unsafe{encoder.encode(frame,false)} .map_err(EncodeError::VideoEncodeFrame)?; - output_file.write_all(&res.data) - .map_err(EncodeError::OutputWriteFile)?; + let mp4_sample=mp4::Mp4Sample{ + start_time:i as u64, + duration:1, + rendering_offset:0, + is_sync:false, + bytes:res.data.into(), + }; + mp4.write_sample(TRACK_ID,&mp4_sample) + .map_err(EncodeError::OutputMp4WriteSample)?; } + mp4.write_end() + .map_err(EncodeError::OutputMp4End)?; + Ok(()) }