Skip to content

Commit 0bf83d7

Browse files
committed
video: Add support for playing F4V (MP4) files
1 parent 6971a1a commit 0bf83d7

File tree

3 files changed

+234
-3
lines changed

3 files changed

+234
-3
lines changed

Cargo.lock

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ ttf-parser = "0.20"
6666
num-bigint = "0.4"
6767
unic-segment = "0.9.0"
6868
id3 = "1.13.1"
69+
mp4parse = "0.17.0"
6970

7071
[target.'cfg(not(target_family = "wasm"))'.dependencies.futures]
7172
version = "0.3.30"

core/src/streams.rs

Lines changed: 200 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use ruffle_video::frame::EncodedFrame;
3333
use ruffle_video::VideoStreamHandle;
3434
use std::cmp::max;
3535
use std::io::{Seek, SeekFrom};
36+
use std::sync::Arc;
3637
use swf::{AudioCompression, SoundFormat, VideoCodec, VideoDeblocking};
3738
use thiserror::Error;
3839
use url::Url;
@@ -176,6 +177,14 @@ pub enum NetStreamType {
176177
/// frame IDs ourselves for various API related purposes.
177178
frame_id: u32,
178179
},
180+
F4v {
181+
context: Arc<mp4parse::MediaContext>,
182+
183+
/// The currently playing video track's stream instance.
184+
video_stream: Option<VideoStreamHandle>,
185+
186+
frame_id: Option<u32>,
187+
},
179188
}
180189

181190
#[derive(Clone, Debug, Collect)]
@@ -514,6 +523,10 @@ impl<'gc> NetStream<'gc> {
514523
.expect("FLV reader stream position") as usize;
515524
}
516525

526+
if matches!(write.stream_type, Some(NetStreamType::F4v { .. })) {
527+
todo!("Seeking in F4V streams");
528+
}
529+
517530
drop(write);
518531

519532
if let Some(AvmObject::Avm2(_)) = self.0.read().avm_object {
@@ -798,8 +811,9 @@ impl<'gc> NetStream<'gc> {
798811
return false;
799812
}
800813

801-
match buffer.get(0..3) {
802-
Some([0x46, 0x4C, 0x56]) => {
814+
match buffer.get(0..8) {
815+
// Only version 1 is valid.
816+
Some([b'F', b'L', b'V', 1, _, _, _, _]) => {
803817
let mut reader = FlvReader::from_parts(&buffer, write.offset);
804818
match FlvHeader::parse(&mut reader) {
805819
Ok(header) => {
@@ -816,11 +830,32 @@ impl<'gc> NetStream<'gc> {
816830
Err(e) => {
817831
//TODO: Fire an error event to AS & stop playing too
818832
tracing::error!("FLV header parsing failed: {}", e);
819-
write.preload_offset = 3;
833+
write.preload_offset = 8; // ???
820834
false
821835
}
822836
}
823837
}
838+
// Video File Format Specification Version 10, page 32
839+
// Flash Player expects a valid F4V file to begin with the one of the following top-level boxes:
840+
// - ftyp (see “ftyp box” on page 18)
841+
// - moov (see “moov box” on page 19)
842+
// - mdat (see “mdat box” on page 32)
843+
// And the first 4 bytes are the length of the first box.
844+
Some([_, _, _, _, b'f', b't', b'y', b'p'])
845+
| Some([_, _, _, _, b'm', b'o', b'o', b'v'])
846+
| Some([_, _, _, _, b'm', b'd', b'a', b't']) => {
847+
println!("F4V");
848+
849+
let mut cursor = slice.as_cursor();
850+
let context = mp4parse::read_mp4(&mut cursor).unwrap();
851+
println!("{:#?}", context);
852+
write.stream_type = Some(NetStreamType::F4v {
853+
context: Arc::new(context),
854+
video_stream: None,
855+
frame_id: None,
856+
});
857+
true
858+
}
824859
Some(magic) => {
825860
//Unrecognized signature
826861
//TODO: Fire an error event to AS & stop playing too
@@ -1189,6 +1224,168 @@ impl<'gc> NetStream<'gc> {
11891224
}
11901225
}
11911226

1227+
if let Some(NetStreamType::F4v {
1228+
context: media_context,
1229+
frame_id,
1230+
video_stream,
1231+
}) = &mut write.stream_type
1232+
{
1233+
println!("frame id: {:?}, end_time: {}", frame_id, max_time);
1234+
1235+
let should_do_frame;
1236+
let sample_id;
1237+
1238+
match frame_id {
1239+
Some(frame) => {
1240+
should_do_frame = max_time > *frame as f64 * 1000.0 / 30.0;
1241+
1242+
if should_do_frame {
1243+
*frame_id = Some(*frame + 1);
1244+
sample_id = frame_id.unwrap();
1245+
} else {
1246+
sample_id = 0;
1247+
}
1248+
}
1249+
None => {
1250+
should_do_frame = true;
1251+
sample_id = 0;
1252+
*frame_id = Some(0);
1253+
}
1254+
}
1255+
1256+
if should_do_frame {
1257+
let sample_sizes = &media_context
1258+
.tracks
1259+
.get(1)
1260+
.unwrap()
1261+
.stsz
1262+
.as_ref()
1263+
.unwrap()
1264+
.sample_sizes;
1265+
let chunk_runs = &media_context
1266+
.tracks
1267+
.get(1)
1268+
.unwrap()
1269+
.stsc
1270+
.as_ref()
1271+
.unwrap()
1272+
.samples;
1273+
1274+
let get_samples_in_chunk = |chunk: u32| -> u32 {
1275+
let mut result = 0;
1276+
for cr in chunk_runs {
1277+
if (cr.first_chunk - 1) > chunk {
1278+
break;
1279+
}
1280+
result = cr.samples_per_chunk;
1281+
}
1282+
result
1283+
};
1284+
1285+
let chunk_of_sample = |sample: u32| -> (u32, u32) {
1286+
let mut sample_accum = 0;
1287+
for chunk in 0.. {
1288+
let samples_in_chunk = get_samples_in_chunk(chunk);
1289+
if sample_accum + samples_in_chunk > sample {
1290+
return (chunk, sample_accum);
1291+
}
1292+
sample_accum += samples_in_chunk;
1293+
}
1294+
(0, 0)
1295+
};
1296+
1297+
let (chunk, first_sample_in_chunk) = chunk_of_sample(sample_id);
1298+
let chunk_offsets = &media_context
1299+
.tracks
1300+
.get(1)
1301+
.unwrap()
1302+
.stco
1303+
.as_ref()
1304+
.unwrap()
1305+
.offsets;
1306+
1307+
let mut offs = chunk_offsets[chunk as usize] as usize;
1308+
1309+
for sam in first_sample_in_chunk..sample_id {
1310+
offs += sample_sizes[sam as usize] as usize;
1311+
}
1312+
1313+
let siz = sample_sizes[sample_id as usize] as usize;
1314+
1315+
println!("offs: {}, siz: {}", offs, siz);
1316+
let s = buffer;
1317+
1318+
let encoded_frame = EncodedFrame {
1319+
codec: VideoCodec::H263, // TODO
1320+
data: s[offs..offs + siz].as_ref(),
1321+
frame_id: frame_id.unwrap(),
1322+
};
1323+
1324+
let video_handle: VideoStreamHandle = match video_stream {
1325+
Some(stream) => *stream,
1326+
None => {
1327+
match context.video.register_video_stream(
1328+
1,
1329+
(8, 8),
1330+
VideoCodec::H263, // TODO
1331+
VideoDeblocking::UseVideoPacketValue,
1332+
) {
1333+
Ok(new_handle) => {
1334+
*video_stream = Some(new_handle);
1335+
1336+
new_handle
1337+
}
1338+
Err(e) => {
1339+
tracing::error!(
1340+
"Got error when registring FLV video stream: {}",
1341+
e
1342+
);
1343+
return; //TODO: This originally breaks and halts tag processing
1344+
}
1345+
}
1346+
}
1347+
};
1348+
1349+
let mdct = media_context.clone();
1350+
let trk = mdct.tracks.get(1).unwrap();
1351+
let stsd = trk.stsd.as_ref().unwrap();
1352+
let descs = stsd.descriptions.get(0).unwrap();
1353+
1354+
match descs {
1355+
mp4parse::SampleEntry::Video(video) => match &video.codec_specific {
1356+
mp4parse::VideoCodecSpecific::AVCConfig(avcconf) => {
1357+
let prel_frame = EncodedFrame {
1358+
codec: VideoCodec::H263, // TODO: H264
1359+
data: avcconf.as_slice(),
1360+
frame_id: frame_id.unwrap(),
1361+
};
1362+
if *frame_id == Some(0) {
1363+
println!("preloading avc config");
1364+
// TODO: this is a misuse of the API, should add/use a dedicated
1365+
// configuration method for this
1366+
let _ = context
1367+
.video
1368+
.preload_video_stream_frame(video_handle, prel_frame);
1369+
}
1370+
}
1371+
mp4parse::VideoCodecSpecific::VPxConfig(_) => todo!(),
1372+
mp4parse::VideoCodecSpecific::AV1Config(_) => todo!(),
1373+
mp4parse::VideoCodecSpecific::ESDSConfig(_) => todo!(),
1374+
mp4parse::VideoCodecSpecific::H263Config(_) => todo!(),
1375+
},
1376+
mp4parse::SampleEntry::Audio(_) => todo!(),
1377+
mp4parse::SampleEntry::Unknown => todo!(),
1378+
}
1379+
1380+
write.last_decoded_bitmap = Some(
1381+
context
1382+
.video
1383+
.decode_video_stream_frame(video_handle, encoded_frame, context.renderer)
1384+
.unwrap(),
1385+
);
1386+
}
1387+
}
1388+
11921389
write.stream_time = last_tag_time;
11931390
if let Err(e) = self.commit_sound_stream(context, &mut write) {
11941391
//TODO: Fire an error event at AS.

0 commit comments

Comments
 (0)