Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions core/src/avm1/globals/sound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@
/// This will be true if `Sound.loadSound` was called with `isStreaming` of `true`.
/// A streaming sound can only have a single active instance.
is_streaming: Cell<bool>,

/// Number of bytes loaded for this sound.
bytes_loaded: Cell<u32>,

/// Total number of bytes for this sound.
bytes_total: Cell<Option<u32>>,
}

impl fmt::Debug for Sound<'_> {
Expand All @@ -98,6 +104,8 @@
position: Cell::new(0),
duration: Cell::new(None),
is_streaming: Cell::new(false),
bytes_loaded: Cell::new(0),
bytes_total: Cell::new(None),
},
))
}
Expand Down Expand Up @@ -146,6 +154,22 @@
self.0.is_streaming.set(is_streaming);
}

pub fn bytes_loaded(self) -> u32 {
self.0.bytes_loaded.get()
}

Check warning on line 159 in core/src/avm1/globals/sound.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered lines (157–159)

pub fn set_bytes_loaded(self, bytes_loaded: u32) {
self.0.bytes_loaded.set(bytes_loaded);
}

pub fn bytes_total(self) -> Option<u32> {
self.0.bytes_total.get()
}

pub fn set_bytes_total(self, bytes_total: Option<u32>) {
self.0.bytes_total.set(bytes_total);
}

fn play(self, play: QueuedPlay<'gc>, context: &mut UpdateContext<'gc>) {
let write = Gc::write(context.gc(), self.0);
let sound_handle = match &mut *unlock!(write, SoundData, state).borrow_mut() {
Expand Down Expand Up @@ -435,21 +459,27 @@
}

fn get_bytes_loaded<'gc>(
activation: &mut Activation<'_, 'gc>,
_this: Object<'gc>,
_activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,

Check warning on line 463 in core/src/avm1/globals/sound.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered lines (462–463)
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
avm1_stub!(activation, "Sound", "getBytesLoaded");
Ok(1.into())
if let NativeObject::Sound(sound) = this.native() {
return Ok(sound.bytes_loaded().into());
}
Ok(Value::Undefined)

Check warning on line 469 in core/src/avm1/globals/sound.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered lines (466–469)
}

fn get_bytes_total<'gc>(
activation: &mut Activation<'_, 'gc>,
_this: Object<'gc>,
_activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
avm1_stub!(activation, "Sound", "getBytesTotal");
Ok(1.into())
if let NativeObject::Sound(sound) = this.native() {
if let Some(total) = sound.bytes_total() {
return Ok(total.into());
}
}

Check warning on line 481 in core/src/avm1/globals/sound.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (481)
Ok(Value::Undefined)
}

fn get_pan<'gc>(
Expand Down
5 changes: 5 additions & 0 deletions core/src/backend/navigator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,11 @@
/// URL (generally only if configured to do so by the user).
fn pre_process_url(&self, url: Url) -> Url;

/// Estimate the length of a local file for a given request.
fn estimate_file_length(&self, _request: &Request) -> Option<u32> {
None
}

Check warning on line 304 in core/src/backend/navigator.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered lines (302–304)

/// Handle any Socket connection request
///
/// Use [SocketAction::Connect] to notify AVM that the connection failed or succeeded.
Expand Down
50 changes: 42 additions & 8 deletions core/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1260,12 +1260,46 @@
request: Request,
is_streaming: bool,
) -> OwnedFuture<(), Error> {
if let Some(file_length) = uc.navigator.estimate_file_length(&request) {
if let NativeObject::Sound(sound) = sound_object.native() {
sound.set_bytes_total(Some(file_length));
}
}

Check warning on line 1267 in core/src/loader.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (1267)

let player = uc.player_handle();
let sound_object = ObjectHandle::stash(uc, sound_object);

Box::pin(async move {
let fetch = player.lock().unwrap().fetch(request, FetchReason::Other);
let response = wait_for_full_response(fetch).await;
let mut response = fetch.await.map_err(|error| error.error)?;

let mut body = Vec::new();
let mut total_loaded = 0u32;

loop {
let chunk = response.next_chunk().await?;
match chunk {
Some(chunk_data) => {
let chunk_len = chunk_data.len();
total_loaded = total_loaded.saturating_add(chunk_len as u32);
body.extend_from_slice(&chunk_data);

player.lock().unwrap().update(|uc| -> Result<(), Error> {
let sound_object = sound_object.fetch(uc);

let NativeObject::Sound(sound) = sound_object.native() else {
panic!("NativeObject must be Sound");

Check warning on line 1291 in core/src/loader.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (1291)
};

// Update bytes_loaded as we receive chunks
sound.set_bytes_loaded(total_loaded);

Ok(())
})?;
}
None => break,
}
}

// Fire the load handler.
player.lock().unwrap().update(|uc| {
Expand All @@ -1277,10 +1311,9 @@

let mut activation = Activation::from_stub(uc, ActivationIdentifier::root("[Loader]"));

let success = response
.map_err(|e| e.error)
.and_then(|(body, _, _, _)| {
let handle = activation.context.audio.register_mp3(&body)?;
let success = match activation.context.audio.register_mp3(&body) {
Ok(handle) => {
sound.set_bytes_total(Some(body.len() as u32));
sound.load_sound(&mut activation, sound_object, handle);
sound.set_duration(Some(0));
sound.load_id3(&mut activation, sound_object, &body)?;
Expand All @@ -1290,9 +1323,10 @@
.get_sound_duration(handle)
.map(|d| d.round() as u32);
sound.set_duration(duration);
Ok(())
})
.is_ok();
true
}
Err(_) => false,

Check warning on line 1328 in core/src/loader.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (1328)
};

let _ = sound_object.call_method(
istr!("onLoad"),
Expand Down
19 changes: 19 additions & 0 deletions frontend-utils/src/backends/navigator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,25 @@ impl<F: FutureSpawner<Error> + 'static, I: NavigatorInterface> NavigatorBackend

tokio::spawn(future);
}

/// Estimate the length of a local file for a given request.
fn estimate_file_length(&self, request: &Request) -> Option<u32> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use SuccessResponse::expected_length()?

Copy link
Contributor Author

@Flawake Flawake Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that one is only available from a future and is not guaranteed to be ready.

    Box::pin(async move {
        let fetch = player.lock().unwrap().fetch(request, FetchReason::Other);
        let mut response = fetch.await.map_err(|error| error.error)?;
...

from core->src->loader.rs line 1273

response is inside the async block.

While flash always has the length ready on local files.

s.loadsound("local_filesystem_file.mp3", b_streaming);
trace(s.getBytesTotal());

always immediatly returns the amount of bytes in flash with local files. Ruffle is not guaranteed to do so in the async block with the response.

Or is it possible to get the SuccessResponse::expected_length() outside of the async block?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the file is not local? Does it block until we get the content length header?

Copy link
Contributor Author

@Flawake Flawake Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, It returns undefined on a non local file. In flash, and in Ruffle with this commit

Copy link
Contributor Author

@Flawake Flawake Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not added a file from a http request in the tests, because I am not sure if we can ensure that it will always be undefined, networking etc... Can we do that without problems?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do that without problems?

I'm not sure how our tests work in this regard, there might be a race condition.

If it returns undefined for remote files, it's fine to return undefined for local files too in this iteration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it returns undefined for remote files, it's fine to return undefined for local files too in this iteration.

But that's not what the flash player does. The player always returns the size for local files

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to return file size synchronously, you'd need to refactor how fetching is done in order to allow supplying this information synchronously. Currently it's not possible because a future is returned that requires an await.

There's also the question of whether it's really done synchronously, or asynchronously but Flash manages to do it before reading the value in AS. We'd need to simulate a delay to reading a file and check whether something blocks in Flash or undefined is returned. I'm not fully convinced it's done synchronously, because Flash was mainly a web technology and it just doesn't make sense to implement it differently for local files compared to remote files.

I feel like the effort involved outweighs the benefits we get from it, that's why I think it's fine to return undefined for now.

let url_str = request.url();
if let Ok(resolved_url) = self.resolve_url(url_str)
&& resolved_url.scheme() == "file"
{
// Strip query parameters for file path conversion
let mut filesystem_url = resolved_url;
filesystem_url.set_query(None);

let path = filesystem_url.to_file_path().ok()?;

if let Ok(metadata) = std::fs::metadata(&path) {
return Some(metadata.len() as u32);
}
}
None
}
}

/// Spawns a new asynchronous task in a tokio runtime, without the current executor needing to belong to tokio
Expand Down
20 changes: 20 additions & 0 deletions tests/framework/src/backends/navigator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,24 @@ impl NavigatorBackend for TestNavigatorBackend {
}));
}
}

fn estimate_file_length(&self, request: &Request) -> Option<u32> {
let url = request.url();

let mut resolved_url = self.resolve_url(url).ok()?;
if resolved_url.scheme() != "file" {
return None;
}

let path_base: VfsPath = self.relative_base_path.clone();

resolved_url.set_query(None);

let decoded = percent_decode_str(resolved_url.path()).decode_utf8().ok()?;

let path = path_base.join(decoded.as_ref()).ok()?;

let metadata = path.metadata().ok()?;
Some(metadata.len as u32)
}
}
3 changes: 3 additions & 0 deletions tests/tests/swfs/avm1/sound_get_bytes_total/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
3224
undefined
undefined
Binary file not shown.
Binary file not shown.
Binary file added tests/tests/swfs/avm1/sound_get_bytes_total/test.swf
Binary file not shown.
1 change: 1 addition & 0 deletions tests/tests/swfs/avm1/sound_get_bytes_total/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
num_frames = 1
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ PASSED: s1.getVolume() == 100 [./Sound.as:105]
PASSED: s1.getVolume() == 95 [./Sound.as:107]
PASSED: s2.getVolume() == 95 [./Sound.as:108]
FAILED: expected: 'undefined' obtained: number [./Sound.as:110]
FAILED: expected: 'undefined' obtained: number [./Sound.as:111]
PASSED: typeof(s1.getBytesTotal()) == 'undefined' [./Sound.as:111]
FAILED: expected: 'boolean' obtained: undefined [./Sound.as:112]
PASSED: typeof(s1.duration) == 'undefined' [./Sound.as:116]
PASSED: typeof(s1.ID3) == 'undefined' [./Sound.as:117]
Expand All @@ -54,7 +54,7 @@ PASSED: typeof(s2.getTransform()) == 'object' [./Sound.as:174]
PASSED: typeof(s2.getVolume()) == 'number' [./Sound.as:175]
PASSED: s2.getVolume() == 100 [./Sound.as:176]
FAILED: expected: 'undefined' obtained: number [./Sound.as:177]
FAILED: expected: 'undefined' obtained: number [./Sound.as:178]
PASSED: typeof(s2.getBytesTotal()) == 'undefined' [./Sound.as:178]
FAILED: expected: 'boolean' obtained: undefined [./Sound.as:179]
PASSED: typeof(s2.duration) == 'undefined' [./Sound.as:183]
PASSED: typeof(s2.ID3) == 'undefined' [./Sound.as:184]
Expand Down Expand Up @@ -96,7 +96,7 @@ PASSED: s3.getVolume() == 80 [./Sound.as:263]
PASSED: s2.getVolume() == 80 [./Sound.as:273]
PASSED: s3.getVolume() == 80 [./Sound.as:274]
FAILED: expected: "undefined" obtained: number [./Sound.as:336]
FAILED: expected: "undefined" obtained: number [./Sound.as:337]
PASSED: typeof(s.getBytesTotal()) == "undefined" [./Sound.as:337]
PASSED: typeof(s.duration) == "undefined" [./Sound.as:338]
FAILED: expected: "undefined" obtained: number [./Sound.as:339]
PASSED: typeof(s.getDuration()) == "undefined" [./Sound.as:340]
Expand All @@ -111,6 +111,6 @@ FAILED: expected: 209 obtained: 0 [./Sound.as:328]
PASSED: s.position == 0 [./Sound.as:329]
FAILED: no onSoundComplete arrived after 3 seconds
FAILED: Tests run 107 (expected 112) [ [./Sound.as:31]]
#passed: 71
#failed: 37
#passed: 74
#failed: 34
#total tests run: 108
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ PASSED: s1.getVolume() == 100 [./Sound.as:105]
PASSED: s1.getVolume() == 95 [./Sound.as:107]
PASSED: s2.getVolume() == 95 [./Sound.as:108]
FAILED: expected: 'undefined' obtained: number [./Sound.as:110]
FAILED: expected: 'undefined' obtained: number [./Sound.as:111]
PASSED: typeof(s1.getBytesTotal()) == 'undefined' [./Sound.as:111]
FAILED: expected: 'boolean' obtained: undefined [./Sound.as:112]
PASSED: typeof(s1.duration) == 'undefined' [./Sound.as:116]
PASSED: typeof(s1.ID3) == 'undefined' [./Sound.as:117]
Expand All @@ -54,7 +54,7 @@ PASSED: typeof(s2.getTransform()) == 'object' [./Sound.as:174]
PASSED: typeof(s2.getVolume()) == 'number' [./Sound.as:175]
PASSED: s2.getVolume() == 100 [./Sound.as:176]
FAILED: expected: 'undefined' obtained: number [./Sound.as:177]
FAILED: expected: 'undefined' obtained: number [./Sound.as:178]
PASSED: typeof(s2.getBytesTotal()) == 'undefined' [./Sound.as:178]
FAILED: expected: 'boolean' obtained: undefined [./Sound.as:179]
PASSED: typeof(s2.duration) == 'undefined' [./Sound.as:183]
PASSED: typeof(s2.ID3) == 'undefined' [./Sound.as:184]
Expand Down Expand Up @@ -96,7 +96,7 @@ PASSED: s3.getVolume() == 80 [./Sound.as:263]
PASSED: s2.getVolume() == 80 [./Sound.as:273]
PASSED: s3.getVolume() == 80 [./Sound.as:274]
FAILED: expected: "undefined" obtained: number [./Sound.as:336]
FAILED: expected: "undefined" obtained: number [./Sound.as:337]
PASSED: typeof(s.getBytesTotal()) == "undefined" [./Sound.as:337]
PASSED: typeof(s.duration) == "undefined" [./Sound.as:338]
FAILED: expected: "undefined" obtained: number [./Sound.as:339]
PASSED: typeof(s.getDuration()) == "undefined" [./Sound.as:340]
Expand All @@ -111,6 +111,6 @@ FAILED: expected: 209 obtained: 0 [./Sound.as:328]
PASSED: s.position == 0 [./Sound.as:329]
FAILED: no onSoundComplete arrived after 3 seconds
FAILED: Tests run 107 (expected 112) [ [./Sound.as:31]]
#passed: 71
#failed: 37
#passed: 74
#failed: 34
#total tests run: 108
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ PASSED: s1.getVolume() == 100 [./Sound.as:105]
PASSED: s1.getVolume() == 95 [./Sound.as:107]
PASSED: s2.getVolume() == 95 [./Sound.as:108]
FAILED: expected: 'undefined' obtained: number [./Sound.as:110]
FAILED: expected: 'undefined' obtained: number [./Sound.as:111]
PASSED: typeof(s1.getBytesTotal()) == 'undefined' [./Sound.as:111]
FAILED: expected: 'boolean' obtained: undefined [./Sound.as:112]
PASSED: typeof(s1.duration) == 'undefined' [./Sound.as:116]
PASSED: typeof(s1.ID3) == 'undefined' [./Sound.as:117]
Expand All @@ -54,7 +54,7 @@ PASSED: typeof(s2.getTransform()) == 'object' [./Sound.as:174]
PASSED: typeof(s2.getVolume()) == 'number' [./Sound.as:175]
PASSED: s2.getVolume() == 100 [./Sound.as:176]
FAILED: expected: 'undefined' obtained: number [./Sound.as:177]
FAILED: expected: 'undefined' obtained: number [./Sound.as:178]
PASSED: typeof(s2.getBytesTotal()) == 'undefined' [./Sound.as:178]
FAILED: expected: 'boolean' obtained: undefined [./Sound.as:179]
PASSED: typeof(s2.duration) == 'undefined' [./Sound.as:183]
PASSED: typeof(s2.ID3) == 'undefined' [./Sound.as:184]
Expand Down Expand Up @@ -96,7 +96,7 @@ PASSED: s3.getVolume() == 80 [./Sound.as:263]
PASSED: s2.getVolume() == 80 [./Sound.as:273]
PASSED: s3.getVolume() == 80 [./Sound.as:274]
FAILED: expected: "undefined" obtained: number [./Sound.as:336]
FAILED: expected: "undefined" obtained: number [./Sound.as:337]
PASSED: typeof(s.getBytesTotal()) == "undefined" [./Sound.as:337]
PASSED: typeof(s.duration) == "undefined" [./Sound.as:338]
FAILED: expected: "undefined" obtained: number [./Sound.as:339]
PASSED: typeof(s.getDuration()) == "undefined" [./Sound.as:340]
Expand All @@ -111,6 +111,6 @@ FAILED: expected: 209 obtained: 0 [./Sound.as:328]
PASSED: s.position == 0 [./Sound.as:329]
FAILED: no onSoundComplete arrived after 3 seconds
FAILED: Tests run 107 (expected 112) [ [./Sound.as:31]]
#passed: 71
#failed: 37
#passed: 74
#failed: 34
#total tests run: 108
Loading