avm1: Store movie URL on load and implement _url (merge #912)

This commit is contained in:
Mike Welsh 2020-07-27 01:38:28 -07:00 committed by GitHub
commit 8ac2ad9b40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 397 additions and 117 deletions

View File

@ -1246,6 +1246,7 @@ impl<'a, 'gc: 'a> Activation<'a, 'gc> {
context.player.clone().unwrap(), context.player.clone().unwrap(),
level, level,
fetch, fetch,
url,
None, None,
); );
context.navigator.spawn_future(process); context.navigator.spawn_future(process);
@ -1336,6 +1337,7 @@ impl<'a, 'gc: 'a> Activation<'a, 'gc> {
context.player.clone().unwrap(), context.player.clone().unwrap(),
clip_target, clip_target,
fetch, fetch,
url.to_string(),
None, None,
); );
context.navigator.spawn_future(process); context.navigator.spawn_future(process);
@ -1353,6 +1355,7 @@ impl<'a, 'gc: 'a> Activation<'a, 'gc> {
context.player.clone().unwrap(), context.player.clone().unwrap(),
level, level,
fetch, fetch,
url.to_string(),
None, None,
); );
context.navigator.spawn_future(process); context.navigator.spawn_future(process);

View File

@ -1096,6 +1096,7 @@ fn load_movie<'gc>(
context.player.clone().unwrap(), context.player.clone().unwrap(),
DisplayObject::MovieClip(target), DisplayObject::MovieClip(target),
fetch, fetch,
url.to_string(),
None, None,
); );

View File

@ -135,6 +135,7 @@ pub fn load_clip<'gc>(
context.player.clone().unwrap(), context.player.clone().unwrap(),
DisplayObject::MovieClip(movieclip), DisplayObject::MovieClip(movieclip),
fetch, fetch,
url.to_string(),
Some(this), Some(this),
); );

View File

@ -898,11 +898,15 @@ fn drop_target<'gc>(
fn url<'gc>( fn url<'gc>(
_activation: &mut Activation<'_, 'gc>, _activation: &mut Activation<'_, 'gc>,
_context: &mut UpdateContext<'_, 'gc, '_>, context: &mut UpdateContext<'_, 'gc, '_>,
_this: DisplayObject<'gc>, this: DisplayObject<'gc>,
) -> Result<Value<'gc>, Error<'gc>> { ) -> Result<Value<'gc>, Error<'gc>> {
log::warn!("Unimplemented property _url"); Ok(this
Ok("".into()) .as_movie_clip()
.and_then(|mc| mc.movie())
.and_then(|mov| mov.url().map(|s| s.to_string()))
.map(|s| AvmString::new(context.gc_context, s).into())
.unwrap_or_else(|| "".into()))
} }
fn high_quality<'gc>( fn high_quality<'gc>(

View File

@ -1,6 +1,7 @@
//! Browser-related platform functions //! Browser-related platform functions
use crate::loader::Error; use crate::loader::Error;
use std::borrow::Cow;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::fs; use std::fs;
use std::future::Future; use std::future::Future;
@ -11,6 +12,57 @@ use std::sync::mpsc::{channel, Receiver, Sender};
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use std::time::Duration; use std::time::Duration;
use swf::avm1::types::SendVarsMethod; use swf::avm1::types::SendVarsMethod;
use url::{ParseError, Url};
/// Attempt to convert a relative filesystem path into an absolute `file:///`
/// URL.
///
/// If the relative path is an absolute path, the base will not be used, but it
/// will still be parsed into a `Url`.
///
/// This is the desktop version of this function, which actually carries out
/// the above instructions. On non-Unix, non-Windows, non-Redox environments,
/// this function always yields an error.
#[cfg(any(unix, windows, target_os = "redox"))]
pub fn url_from_relative_path<P: AsRef<Path>>(base: P, relative: &str) -> Result<Url, ParseError> {
let parsed = Url::from_file_path(relative);
if let Err(()) = parsed {
let base =
Url::from_directory_path(base).map_err(|_| ParseError::RelativeUrlWithoutBase)?;
return base.join(relative);
}
Ok(parsed.unwrap())
}
/// Attempt to convert a relative filesystem path into an absolute `file:///`
/// URL.
///
/// If the relative path is an absolute path, the base will not be used, but it
/// will still be parsed into a `Url`.
///
/// This is the web version of this function, which always yields an error. On
/// Unix, Windows, or Redox, this function actually carries out the above
/// instructions.
#[cfg(not(any(unix, windows, target_os = "redox")))]
pub fn url_from_relative_path<P: AsRef<Path>>(base: P, relative: &str) -> Result<Url, ParseError> {
Err(ParseError::RelativeUrlWithoutBase)
}
/// Attempt to convert a relative URL into an absolute URL, using the base URL
/// if necessary.
///
/// If the relative URL is actually absolute, then the base will not be used.
pub fn url_from_relative_url(base: &str, relative: &str) -> Result<Url, ParseError> {
let parsed = Url::parse(relative);
if let Err(ParseError::RelativeUrlWithoutBase) = parsed {
let base = Url::parse(base)?;
return base.join(relative);
}
parsed
}
/// Enumerates all possible navigation methods. /// Enumerates all possible navigation methods.
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
@ -132,6 +184,16 @@ pub trait NavigatorBackend {
/// TODO: For some reason, `wasm_bindgen_futures` wants unpinnable futures. /// TODO: For some reason, `wasm_bindgen_futures` wants unpinnable futures.
/// This seems highly limiting. /// This seems highly limiting.
fn spawn_future(&mut self, future: OwnedFuture<(), Error>); fn spawn_future(&mut self, future: OwnedFuture<(), Error>);
/// Resolve a relative URL.
///
/// This function must not change URLs which are already protocol, domain,
/// and path absolute. For URLs that are relative, the implementator of
/// this function may opt to convert them to absolute using an implementor
/// defined base. For a web browser, the most obvious base would be the
/// current document's base URL, while the most obvious base for a desktop
/// client would be the file-URL form of the current path.
fn resolve_relative_url<'a>(&mut self, url: &'a str) -> Cow<'a, str>;
} }
/// A null implementation of an event loop that only supports blocking. /// A null implementation of an event loop that only supports blocking.
@ -303,4 +365,13 @@ impl NavigatorBackend for NullNavigatorBackend {
channel.send(future).unwrap(); channel.send(future).unwrap();
} }
} }
fn resolve_relative_url<'a>(&mut self, url: &'a str) -> Cow<'a, str> {
let relative = url_from_relative_path(&self.relative_base_path, url);
if let Ok(relative) = relative {
relative.into_string().into()
} else {
url.into()
}
}
} }

View File

@ -34,7 +34,8 @@ impl<'gc> Button<'gc> {
) -> Self { ) -> Self {
let mut actions = vec![]; let mut actions = vec![];
for action in &button.actions { for action in &button.actions {
let action_data = source_movie.owned_subslice(action.action_data.clone()); let action_data =
source_movie.owned_subslice(action.action_data.clone(), &source_movie.movie);
for condition in &action.conditions { for condition in &action.conditions {
let button_action = ButtonAction { let button_action = ButtonAction {
action_data: action_data.clone(), action_data: action_data.clone(),

View File

@ -2567,7 +2567,7 @@ impl ClipAction {
let len = other.action_data.len(); let len = other.action_data.len();
let key_code = other.key_code; let key_code = other.key_code;
let movie = Arc::new(movie.from_movie_and_subdata(other.action_data)); let movie = Arc::new(movie.from_movie_and_subdata(other.action_data, &movie));
other.events.into_iter().map(move |event| Self { other.events.into_iter().map(move |event| Self {
event: match event { event: match event {
ClipEventFlag::Construct => ClipEvent::Construct, ClipEventFlag::Construct => ClipEvent::Construct,

View File

@ -424,6 +424,7 @@ mod tests {
use crate::font::{EvalParameters, Font}; use crate::font::{EvalParameters, Font};
use crate::player::{Player, DEVICE_FONT_TAG}; use crate::player::{Player, DEVICE_FONT_TAG};
use gc_arena::{rootless_arena, MutationContext}; use gc_arena::{rootless_arena, MutationContext};
use std::ops::DerefMut;
use swf::Twips; use swf::Twips;
fn with_device_font<F>(callback: F) fn with_device_font<F>(callback: F)
@ -432,7 +433,8 @@ mod tests {
{ {
rootless_arena(|mc| { rootless_arena(|mc| {
let mut renderer: Box<dyn RenderBackend> = Box::new(NullRenderer::new()); let mut renderer: Box<dyn RenderBackend> = Box::new(NullRenderer::new());
let device_font = Player::load_device_font(mc, DEVICE_FONT_TAG, &mut renderer).unwrap(); let device_font =
Player::load_device_font(mc, DEVICE_FONT_TAG, renderer.deref_mut()).unwrap();
callback(mc, device_font); callback(mc, device_font);
}) })

View File

@ -22,6 +22,9 @@ pub enum Error {
#[error("Load cancelled")] #[error("Load cancelled")]
Cancelled, Cancelled,
#[error("Non-root-movie loader spawned as root movie loader")]
NotRootMovieLoader,
#[error("Non-movie loader spawned as movie loader")] #[error("Non-movie loader spawned as movie loader")]
NotMovieLoader, NotMovieLoader,
@ -34,6 +37,9 @@ pub enum Error {
#[error("Non-XML loader spawned as XML loader")] #[error("Non-XML loader spawned as XML loader")]
NotXmlLoader, NotXmlLoader,
#[error("Could not fetch movie {0}")]
FetchError(String),
#[error("Invalid SWF")] #[error("Invalid SWF")]
InvalidSwf(#[from] crate::tag_utils::Error), InvalidSwf(#[from] crate::tag_utils::Error),
@ -108,6 +114,27 @@ impl<'gc> LoadManager<'gc> {
self.0.get_mut(handle) self.0.get_mut(handle)
} }
/// Kick off the root movie load.
///
/// The root movie is special because it determines a few bits of player
/// state, such as the size of the stage and the current frame rate. Ergo,
/// this method should only be called once, by the player that is trying to
/// kick off it's root movie load.
pub fn load_root_movie(
&mut self,
player: Weak<Mutex<Player>>,
fetch: OwnedFuture<Vec<u8>, Error>,
url: String,
) -> OwnedFuture<(), Error> {
let loader = Loader::RootMovie { self_handle: None };
let handle = self.add_loader(loader);
let loader = self.get_loader_mut(handle).unwrap();
loader.introduce_loader_handle(handle);
loader.root_movie_loader(player, fetch, url)
}
/// Kick off a movie clip load. /// Kick off a movie clip load.
/// ///
/// Returns the loader's async process, which you will need to spawn. /// Returns the loader's async process, which you will need to spawn.
@ -116,6 +143,7 @@ impl<'gc> LoadManager<'gc> {
player: Weak<Mutex<Player>>, player: Weak<Mutex<Player>>,
target_clip: DisplayObject<'gc>, target_clip: DisplayObject<'gc>,
fetch: OwnedFuture<Vec<u8>, Error>, fetch: OwnedFuture<Vec<u8>, Error>,
url: String,
target_broadcaster: Option<Object<'gc>>, target_broadcaster: Option<Object<'gc>>,
) -> OwnedFuture<(), Error> { ) -> OwnedFuture<(), Error> {
let loader = Loader::Movie { let loader = Loader::Movie {
@ -129,7 +157,7 @@ impl<'gc> LoadManager<'gc> {
let loader = self.get_loader_mut(handle).unwrap(); let loader = self.get_loader_mut(handle).unwrap();
loader.introduce_loader_handle(handle); loader.introduce_loader_handle(handle);
loader.movie_loader(player, fetch) loader.movie_loader(player, fetch, url)
} }
/// Indicates that a movie clip has initialized (ran it's first frame). /// Indicates that a movie clip has initialized (ran it's first frame).
@ -229,6 +257,12 @@ impl<'gc> Default for LoadManager<'gc> {
/// A struct that holds garbage-collected pointers for asynchronous code. /// A struct that holds garbage-collected pointers for asynchronous code.
pub enum Loader<'gc> { pub enum Loader<'gc> {
/// Loader that is loading the root movie of a player.
RootMovie {
/// The handle to refer to this loader instance.
self_handle: Option<Handle>,
},
/// Loader that is loading a new movie into a movieclip. /// Loader that is loading a new movie into a movieclip.
Movie { Movie {
/// The handle to refer to this loader instance. /// The handle to refer to this loader instance.
@ -291,6 +325,7 @@ pub enum Loader<'gc> {
unsafe impl<'gc> Collect for Loader<'gc> { unsafe impl<'gc> Collect for Loader<'gc> {
fn trace(&self, cc: CollectionContext) { fn trace(&self, cc: CollectionContext) {
match self { match self {
Loader::RootMovie { .. } => {}
Loader::Movie { Loader::Movie {
target_clip, target_clip,
target_broadcaster, target_broadcaster,
@ -313,6 +348,7 @@ impl<'gc> Loader<'gc> {
/// run. /// run.
pub fn introduce_loader_handle(&mut self, handle: Handle) { pub fn introduce_loader_handle(&mut self, handle: Handle) {
match self { match self {
Loader::RootMovie { self_handle, .. } => *self_handle = Some(handle),
Loader::Movie { self_handle, .. } => *self_handle = Some(handle), Loader::Movie { self_handle, .. } => *self_handle = Some(handle),
Loader::Form { self_handle, .. } => *self_handle = Some(handle), Loader::Form { self_handle, .. } => *self_handle = Some(handle),
Loader::LoadVars { self_handle, .. } => *self_handle = Some(handle), Loader::LoadVars { self_handle, .. } => *self_handle = Some(handle),
@ -320,6 +356,46 @@ impl<'gc> Loader<'gc> {
} }
} }
/// Construct a future for the root movie loader.
pub fn root_movie_loader(
&mut self,
player: Weak<Mutex<Player>>,
fetch: OwnedFuture<Vec<u8>, Error>,
mut url: String,
) -> OwnedFuture<(), Error> {
let _handle = match self {
Loader::RootMovie { self_handle, .. } => {
self_handle.expect("Loader not self-introduced")
}
_ => return Box::pin(async { Err(Error::NotMovieLoader) }),
};
let player = player
.upgrade()
.expect("Could not upgrade weak reference to player");
Box::pin(async move {
player.lock().expect("Could not lock player!!").update(
|_avm1, _avm2, uc| -> Result<(), Error> {
url = uc.navigator.resolve_relative_url(&url).into_owned();
Ok(())
},
)?;
let data = (fetch.await)
.and_then(|data| Ok((data.len(), SwfMovie::from_data(&data, Some(url.clone()))?)));
if let Ok((_length, movie)) = data {
player.lock().unwrap().set_root_movie(Arc::new(movie));
Ok(())
} else {
Err(Error::FetchError(url))
}
})
}
/// Construct a future for the given movie loader. /// Construct a future for the given movie loader.
/// ///
/// The given future should be passed immediately to an executor; it will /// The given future should be passed immediately to an executor; it will
@ -331,6 +407,7 @@ impl<'gc> Loader<'gc> {
&mut self, &mut self,
player: Weak<Mutex<Player>>, player: Weak<Mutex<Player>>,
fetch: OwnedFuture<Vec<u8>, Error>, fetch: OwnedFuture<Vec<u8>, Error>,
mut url: String,
) -> OwnedFuture<(), Error> { ) -> OwnedFuture<(), Error> {
let handle = match self { let handle = match self {
Loader::Movie { self_handle, .. } => self_handle.expect("Loader not self-introduced"), Loader::Movie { self_handle, .. } => self_handle.expect("Loader not self-introduced"),
@ -344,6 +421,8 @@ impl<'gc> Loader<'gc> {
Box::pin(async move { Box::pin(async move {
player.lock().expect("Could not lock player!!").update( player.lock().expect("Could not lock player!!").update(
|avm1, _avm2, uc| -> Result<(), Error> { |avm1, _avm2, uc| -> Result<(), Error> {
url = uc.navigator.resolve_relative_url(&url).into_owned();
let (clip, broadcaster) = match uc.load_manager.get_loader(handle) { let (clip, broadcaster) = match uc.load_manager.get_loader(handle) {
Some(Loader::Movie { Some(Loader::Movie {
target_clip, target_clip,
@ -375,7 +454,8 @@ impl<'gc> Loader<'gc> {
}, },
)?; )?;
let data = (fetch.await).and_then(|data| Ok((data.len(), SwfMovie::from_data(&data)?))); let data = (fetch.await)
.and_then(|data| Ok((data.len(), SwfMovie::from_data(&data, Some(url.clone()))?)));
if let Ok((length, movie)) = data { if let Ok((length, movie)) = data {
let movie = Arc::new(movie); let movie = Arc::new(movie);

View File

@ -6,10 +6,9 @@ use crate::avm1::object::Object;
use crate::avm1::{Avm1, AvmString, TObject, Timers, Value}; use crate::avm1::{Avm1, AvmString, TObject, Timers, Value};
use crate::avm2::Avm2; use crate::avm2::Avm2;
use crate::backend::input::{InputBackend, MouseCursor}; use crate::backend::input::{InputBackend, MouseCursor};
use crate::backend::navigator::{NavigatorBackend, RequestOptions};
use crate::backend::storage::StorageBackend; use crate::backend::storage::StorageBackend;
use crate::backend::{ use crate::backend::{audio::AudioBackend, render::Letterbox, render::RenderBackend};
audio::AudioBackend, navigator::NavigatorBackend, render::Letterbox, render::RenderBackend,
};
use crate::context::{ActionQueue, ActionType, RenderContext, UpdateContext}; use crate::context::{ActionQueue, ActionType, RenderContext, UpdateContext};
use crate::display_object::{EditText, MorphShape, MovieClip}; use crate::display_object::{EditText, MorphShape, MovieClip};
use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode, PlayerEvent}; use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode, PlayerEvent};
@ -183,29 +182,21 @@ pub struct Player {
impl Player { impl Player {
pub fn new( pub fn new(
mut renderer: Renderer, renderer: Renderer,
audio: Audio, audio: Audio,
navigator: Navigator, navigator: Navigator,
input: Input, input: Input,
movie: SwfMovie,
storage: Storage, storage: Storage,
) -> Result<Arc<Mutex<Self>>, Error> { ) -> Result<Arc<Mutex<Self>>, Error> {
let movie = Arc::new(movie); let fake_movie = Arc::new(SwfMovie::empty(NEWEST_PLAYER_VERSION));
let movie_width = 550;
info!( let movie_height = 400;
"Loaded SWF version {}, with a resolution of {}x{}", let frame_rate = 12.0;
movie.header().version,
movie.header().stage_size.x_max,
movie.header().stage_size.y_max
);
let movie_width = movie.width();
let movie_height = movie.height();
let mut player = Player { let mut player = Player {
player_version: NEWEST_PLAYER_VERSION, player_version: NEWEST_PLAYER_VERSION,
swf: movie.clone(), swf: fake_movie,
is_playing: false, is_playing: false,
needs_render: true, needs_render: true,
@ -223,26 +214,10 @@ impl Player {
rng: SmallRng::from_seed([0u8; 16]), // TODO(Herschel): Get a proper seed on all platforms. rng: SmallRng::from_seed([0u8; 16]), // TODO(Herschel): Get a proper seed on all platforms.
gc_arena: GcArena::new(ArenaParameters::default(), |gc_context| { gc_arena: GcArena::new(ArenaParameters::default(), |gc_context| {
// Load and parse the device font.
let device_font =
match Self::load_device_font(gc_context, DEVICE_FONT_TAG, &mut renderer) {
Ok(font) => Some(font),
Err(e) => {
log::error!("Unable to load device font: {}", e);
None
}
};
let mut library = Library::default();
library
.library_for_movie_mut(movie.clone())
.set_device_font(device_font);
GcRoot(GcCell::allocate( GcRoot(GcCell::allocate(
gc_context, gc_context,
GcRootData { GcRootData {
library, library: Library::default(),
levels: BTreeMap::new(), levels: BTreeMap::new(),
mouse_hovered_object: None, mouse_hovered_object: None,
drag_object: None, drag_object: None,
@ -257,7 +232,7 @@ impl Player {
)) ))
}), }),
frame_rate: movie.header().frame_rate.into(), frame_rate,
frame_accumulator: 0.0, frame_accumulator: 0.0,
movie_width, movie_width,
@ -281,14 +256,77 @@ impl Player {
storage, storage,
}; };
player.mutate_with_update_context(|avm1, _avm2, context| { player.build_matrices();
player.audio.set_frame_rate(frame_rate);
let player_box = Arc::new(Mutex::new(player));
let mut player_lock = player_box.lock().unwrap();
player_lock.self_reference = Some(Arc::downgrade(&player_box));
std::mem::drop(player_lock);
Ok(player_box)
}
/// Fetch the root movie.
///
/// This should not be called if a root movie fetch has already been kicked
/// off.
pub fn fetch_root_movie(&mut self, movie_url: &str) {
self.mutate_with_update_context(|_avm1, _avm2, context| {
let fetch = context.navigator.fetch(movie_url, RequestOptions::get());
let process = context.load_manager.load_root_movie(
context.player.clone().unwrap(),
fetch,
movie_url.to_string(),
);
context.navigator.spawn_future(process);
});
}
/// Change the root movie.
///
/// This should only be called once, as it makes no attempt at removing
/// previous stage contents. If you need to load a new root movie, you
/// should destroy and recreate the player instance.
pub fn set_root_movie(&mut self, movie: Arc<SwfMovie>) {
info!(
"Loaded SWF version {}, with a resolution of {}x{}",
movie.header().version,
movie.header().stage_size.x_max,
movie.header().stage_size.y_max
);
self.movie_width = movie.width();
self.movie_height = movie.height();
self.frame_rate = movie.header().frame_rate.into();
self.swf = movie;
self.mutate_with_update_context(|avm1, _avm2, context| {
let mut root: DisplayObject = let mut root: DisplayObject =
MovieClip::from_movie(context.gc_context, movie.clone()).into(); MovieClip::from_movie(context.gc_context, context.swf.clone()).into();
root.set_depth(context.gc_context, 0); root.set_depth(context.gc_context, 0);
root.post_instantiation(avm1, context, root, None, false); root.post_instantiation(avm1, context, root, None, false);
root.set_name(context.gc_context, ""); root.set_name(context.gc_context, "");
context.levels.insert(0, root); context.levels.insert(0, root);
// Load and parse the device font.
let device_font =
match Self::load_device_font(context.gc_context, DEVICE_FONT_TAG, context.renderer)
{
Ok(font) => Some(font),
Err(e) => {
log::error!("Unable to load device font: {}", e);
None
}
};
context
.library
.library_for_movie_mut(context.swf.clone())
.set_device_font(device_font);
// Set the version parameter on the root.
let mut activation = Activation::from_nothing( let mut activation = Activation::from_nothing(
avm1, avm1,
ActivationIdentifier::root("[Version Setter]"), ActivationIdentifier::root("[Version Setter]"),
@ -310,15 +348,9 @@ impl Player {
); );
}); });
player.build_matrices(); self.build_matrices();
player.preload(); self.preload();
self.audio.set_frame_rate(self.frame_rate);
let player_box = Arc::new(Mutex::new(player));
let mut player_lock = player_box.lock().unwrap();
player_lock.self_reference = Some(Arc::downgrade(&player_box));
std::mem::drop(player_lock);
Ok(player_box)
} }
pub fn tick(&mut self, dt: f64) { pub fn tick(&mut self, dt: f64) {
@ -1066,14 +1098,11 @@ impl Player {
pub fn load_device_font<'gc>( pub fn load_device_font<'gc>(
gc_context: gc_arena::MutationContext<'gc, '_>, gc_context: gc_arena::MutationContext<'gc, '_>,
data: &[u8], data: &[u8],
renderer: &mut Renderer, renderer: &mut dyn RenderBackend,
) -> Result<crate::font::Font<'gc>, Error> { ) -> Result<crate::font::Font<'gc>, Error> {
let mut reader = swf::read::Reader::new(data, 8); let mut reader = swf::read::Reader::new(data, 8);
let device_font = crate::font::Font::from_swf_tag( let device_font =
gc_context, crate::font::Font::from_swf_tag(gc_context, renderer, &reader.read_define_font_2(3)?)?;
renderer.deref_mut(),
&reader.read_define_font_2(3)?,
)?;
Ok(device_font) Ok(device_font)
} }

View File

@ -1,3 +1,4 @@
use crate::backend::navigator::url_from_relative_path;
use gc_arena::Collect; use gc_arena::Collect;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
@ -17,6 +18,9 @@ pub struct SwfMovie {
/// Uncompressed SWF data. /// Uncompressed SWF data.
data: Vec<u8>, data: Vec<u8>,
/// The URL the SWF was downloaded from.
url: Option<String>,
} }
impl SwfMovie { impl SwfMovie {
@ -31,25 +35,37 @@ impl SwfMovie {
num_frames: 0, num_frames: 0,
}, },
data: vec![], data: vec![],
url: None,
} }
} }
/// Construct a movie from an existing movie with any particular data on it. /// Construct a movie from an existing movie with any particular data on
pub fn from_movie_and_subdata(&self, data: Vec<u8>) -> Self { /// it.
///
/// Use of this method is discouraged. SWF data should be borrowed or
/// sliced as necessary to refer to partial sections of a file.
pub fn from_movie_and_subdata(&self, data: Vec<u8>, source: &SwfMovie) -> Self {
Self { Self {
header: self.header.clone(), header: self.header.clone(),
data, data,
url: source.url.clone(),
} }
} }
/// Utility method to construct a movie from a file on disk. /// Utility method to construct a movie from a file on disk.
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Error> { pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let mut url = path.as_ref().to_string_lossy().to_owned().to_string();
let cwd = std::env::current_dir()?;
if let Ok(abs_url) = url_from_relative_path(cwd, &url) {
url = abs_url.into_string();
}
let data = std::fs::read(path)?; let data = std::fs::read(path)?;
Self::from_data(&data) Self::from_data(&data, Some(url))
} }
/// Construct a movie based on the contents of the SWF datastream. /// Construct a movie based on the contents of the SWF datastream.
pub fn from_data(swf_data: &[u8]) -> Result<Self, Error> { pub fn from_data(swf_data: &[u8], url: Option<String>) -> Result<Self, Error> {
let swf_stream = swf::read::read_swf_header(&swf_data[..])?; let swf_stream = swf::read::read_swf_header(&swf_data[..])?;
let header = swf_stream.header; let header = swf_stream.header;
let mut reader = swf_stream.reader; let mut reader = swf_stream.reader;
@ -74,7 +90,7 @@ impl SwfMovie {
data data
}; };
Ok(Self { header, data }) Ok(Self { header, data, url })
} }
pub fn header(&self) -> &Header { pub fn header(&self) -> &Header {
@ -97,6 +113,11 @@ impl SwfMovie {
pub fn height(&self) -> u32 { pub fn height(&self) -> u32 {
(self.header.stage_size.y_max - self.header.stage_size.y_min).to_pixels() as u32 (self.header.stage_size.y_max - self.header.stage_size.y_min).to_pixels() as u32
} }
/// Get the URL this SWF was fetched from.
pub fn url(&self) -> Option<&str> {
self.url.as_deref()
}
} }
/// A shared-ownership reference to some portion of an SWF datastream. /// A shared-ownership reference to some portion of an SWF datastream.
@ -141,12 +162,12 @@ impl SwfSlice {
/// Construct a new slice with a given dataset only. /// Construct a new slice with a given dataset only.
/// ///
/// This is used primarily for converting owned data back into a slice: we /// This is used primarily for converting owned data back into a slice: we
/// reattach the SWF data that we can /// reattach the SWF data to a fresh movie and return a new slice into it.
pub fn owned_subslice(&self, data: Vec<u8>) -> Self { pub fn owned_subslice(&self, data: Vec<u8>, source: &SwfMovie) -> Self {
let len = data.len(); let len = data.len();
Self { Self {
movie: Arc::new(self.movie.from_movie_and_subdata(data)), movie: Arc::new(self.movie.from_movie_and_subdata(data, source)),
start: 0, start: 0,
end: len, end: len,
} }

View File

@ -13,6 +13,7 @@ use ruffle_core::tag_utils::SwfMovie;
use ruffle_core::Player; use ruffle_core::Player;
use std::cell::RefCell; use std::cell::RefCell;
use std::path::Path; use std::path::Path;
use std::sync::Arc;
type Error = Box<dyn std::error::Error>; type Error = Box<dyn std::error::Error>;
@ -366,9 +367,9 @@ fn run_swf(swf_path: &str, num_frames: u32) -> Result<String, Error> {
Box::new(NullAudioBackend::new()), Box::new(NullAudioBackend::new()),
Box::new(NullNavigatorBackend::with_base_path(base_path, channel)), Box::new(NullNavigatorBackend::with_base_path(base_path, channel)),
Box::new(NullInputBackend::new()), Box::new(NullInputBackend::new()),
movie,
Box::new(MemoryStorageBackend::default()), Box::new(MemoryStorageBackend::default()),
)?; )?;
player.lock().unwrap().set_root_movie(Arc::new(movie));
for _ in 0..num_frames { for _ in 0..num_frames {
player.lock().unwrap().run_frame(); player.lock().unwrap().run_frame();

View File

@ -16,6 +16,7 @@ use ruffle_core::{
}; };
use ruffle_render_wgpu::WgpuRenderBackend; use ruffle_render_wgpu::WgpuRenderBackend;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use structopt::StructOpt; use structopt::StructOpt;
@ -92,7 +93,8 @@ fn run_player(input_path: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let storage = Box::new(DiskStorageBackend::new( let storage = Box::new(DiskStorageBackend::new(
input_path.file_name().unwrap_or_default().as_ref(), input_path.file_name().unwrap_or_default().as_ref(),
)); ));
let player = Player::new(renderer, audio, navigator, input, movie, storage)?; let player = Player::new(renderer, audio, navigator, input, storage)?;
player.lock().unwrap().set_root_movie(Arc::new(movie));
player.lock().unwrap().set_is_playing(true); // Desktop player will auto-play. player.lock().unwrap().set_is_playing(true); // Desktop player will auto-play.
player player

View File

@ -2,9 +2,10 @@
use crate::custom_event::RuffleEvent; use crate::custom_event::RuffleEvent;
use ruffle_core::backend::navigator::{ use ruffle_core::backend::navigator::{
NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions, url_from_relative_path, NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions,
}; };
use ruffle_core::loader::Error; use ruffle_core::loader::Error;
use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -129,4 +130,13 @@ impl NavigatorBackend for ExternalNavigatorBackend {
); );
} }
} }
fn resolve_relative_url<'a>(&mut self, url: &'a str) -> Cow<'a, str> {
let relative = url_from_relative_path(&self.relative_base_path, url);
if let Ok(relative) = relative {
relative.into_string().into()
} else {
url.into()
}
}
} }

View File

@ -13,6 +13,7 @@ use std::error::Error;
use std::fs::create_dir_all; use std::fs::create_dir_all;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc;
use structopt::StructOpt; use structopt::StructOpt;
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
@ -84,7 +85,6 @@ fn take_screenshot(
Box::new(NullAudioBackend::new()), Box::new(NullAudioBackend::new()),
Box::new(NullNavigatorBackend::new()), Box::new(NullNavigatorBackend::new()),
Box::new(NullInputBackend::new()), Box::new(NullInputBackend::new()),
movie,
Box::new(MemoryStorageBackend::default()), Box::new(MemoryStorageBackend::default()),
)?; )?;
@ -92,6 +92,7 @@ fn take_screenshot(
.lock() .lock()
.unwrap() .unwrap()
.set_viewport_dimensions(width, height); .set_viewport_dimensions(width, height);
player.lock().unwrap().set_root_movie(Arc::new(movie));
let mut result = Vec::new(); let mut result = Vec::new();
let totalframes = frames + skipframes; let totalframes = frames + skipframes;

View File

@ -116,28 +116,48 @@ exports.RufflePlayer = class RufflePlayer extends HTMLElement {
return false; return false;
} }
/**
* Ensure a fresh Ruffle instance is ready on this player before continuing.
*
* @throws Any exceptions generated by loading Ruffle Core will be logged
* and passed on.
*/
async ensure_fresh_instance() {
if (this.instance) {
this.instance.destroy();
this.instance = null;
console.log("Ruffle instance destroyed.");
}
let Ruffle = await this.Ruffle.catch(function (e) {
console.error("Serious error loading Ruffle: " + e);
throw e;
});
this.instance = Ruffle.new(this.container);
console.log("New Ruffle instance created.");
}
/**
* Load a movie into this Ruffle Player instance by URL.
*
* Any existing movie will be immediately stopped, while the new movie's
* load happens asynchronously. There is currently no way to await the file
* being loaded, or any errors that happen loading it.
*
* @param {String} url The URL to stream.
*/
async stream_swf_url(url) { async stream_swf_url(url) {
//TODO: Actually stream files... //TODO: Actually stream files...
try { try {
if (this.isConnected && !this.is_unused_fallback_object()) { if (this.isConnected && !this.is_unused_fallback_object()) {
let abs_url = new URL(url, window.location.href).toString();
console.log("Loading SWF file " + url); console.log("Loading SWF file " + url);
let response = await fetch(abs_url); await this.ensure_fresh_instance();
this.instance.stream_from(url);
if (response.ok) { if (this.play_button) {
let data = await response.arrayBuffer(); this.play_button.style.display = "block";
await this.play_swf_data(data);
console.log("Playing " + url);
} else {
console.error(
"SWF load failed: " +
response.status +
" " +
response.statusText +
" for " +
url
);
} }
} else { } else {
console.warn( console.warn(
@ -159,22 +179,23 @@ exports.RufflePlayer = class RufflePlayer extends HTMLElement {
} }
} }
/**
* Load a movie's data into this Ruffle Player instance.
*
* Any existing movie will be immediately stopped, and the new movie's data
* placed into a fresh Stage on the same stack.
*
* Please note that by doing this, no URL information will be provided to
* the movie being loaded.
*
* @param {String} url The URL to stream.
*/
async play_swf_data(data) { async play_swf_data(data) {
if (this.isConnected && !this.is_unused_fallback_object()) { if (this.isConnected && !this.is_unused_fallback_object()) {
console.log("Got SWF data"); console.log("Got SWF data");
if (this.instance) { await this.ensure_fresh_instance();
this.instance.destroy(); this.instance.load_data(new Uint8Array(data));
this.instance = null;
console.log("Ruffle instance destroyed.");
}
let Ruffle = await this.Ruffle.catch(function (e) {
console.error("Serious error loading Ruffle: " + e);
throw e;
});
this.instance = Ruffle.new(this.container, new Uint8Array(data));
console.log("New Ruffle instance created."); console.log("New Ruffle instance created.");
if (this.play_button) { if (this.play_button) {

View File

@ -14,7 +14,6 @@ use ruffle_core::backend::storage::StorageBackend;
use ruffle_core::tag_utils::SwfMovie; use ruffle_core::tag_utils::SwfMovie;
use ruffle_core::PlayerEvent; use ruffle_core::PlayerEvent;
use ruffle_web_common::JsResult; use ruffle_web_common::JsResult;
use std::mem::drop;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::{cell::RefCell, error::Error, num::NonZeroI32}; use std::{cell::RefCell, error::Error, num::NonZeroI32};
use wasm_bindgen::{prelude::*, JsCast, JsValue}; use wasm_bindgen::{prelude::*, JsCast, JsValue};
@ -57,8 +56,38 @@ pub struct Ruffle(Index);
#[wasm_bindgen] #[wasm_bindgen]
impl Ruffle { impl Ruffle {
pub fn new(parent: HtmlElement, swf_data: Uint8Array) -> Result<Ruffle, JsValue> { pub fn new(parent: HtmlElement) -> Result<Ruffle, JsValue> {
Ruffle::new_internal(parent, swf_data).map_err(|_| "Error creating player".into()) Ruffle::new_internal(parent).map_err(|_| "Error creating player".into())
}
/// Stream an arbitrary movie file from (presumably) the Internet.
///
/// This method should only be called once per player.
pub fn stream_from(&mut self, movie_url: &str) {
INSTANCES.with(|instances| {
let mut instances = instances.borrow_mut();
let instance = instances.get_mut(self.0).unwrap();
instance.core.lock().unwrap().fetch_root_movie(movie_url);
});
}
/// Play an arbitrary movie on this instance.
///
/// This method should only be called once per player.
pub fn load_data(&mut self, swf_data: Uint8Array) -> Result<(), JsValue> {
let movie = Arc::new({
let mut data = vec![0; swf_data.length() as usize];
swf_data.copy_to(&mut data[..]);
SwfMovie::from_data(&data, None).map_err(|e| format!("Error loading movie: {}", e))?
});
INSTANCES.with(|instances| {
let mut instances = instances.borrow_mut();
let instance = instances.get_mut(self.0).unwrap();
instance.core.lock().unwrap().set_root_movie(movie);
});
Ok(())
} }
pub fn play(&mut self) { pub fn play(&mut self) {
@ -106,16 +135,10 @@ impl Ruffle {
} }
impl Ruffle { impl Ruffle {
fn new_internal(parent: HtmlElement, swf_data: Uint8Array) -> Result<Ruffle, Box<dyn Error>> { fn new_internal(parent: HtmlElement) -> Result<Ruffle, Box<dyn Error>> {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
let _ = console_log::init_with_level(log::Level::Trace); let _ = console_log::init_with_level(log::Level::Trace);
let movie = {
let mut data = vec![0; swf_data.length() as usize];
swf_data.copy_to(&mut data[..]);
SwfMovie::from_data(&data)?
};
let window = web_sys::window().ok_or_else(|| "Expected window")?; let window = web_sys::window().ok_or_else(|| "Expected window")?;
let document = window.document().ok_or("Expected document")?; let document = window.document().ok_or("Expected document")?;
@ -138,12 +161,7 @@ impl Ruffle {
}) })
.unwrap_or_else(|| Box::new(MemoryStorageBackend::default())); .unwrap_or_else(|| Box::new(MemoryStorageBackend::default()));
let core = let core = ruffle_core::Player::new(renderer, audio, navigator, input, local_storage)?;
ruffle_core::Player::new(renderer, audio, navigator, input, movie, local_storage)?;
let mut core_lock = core.lock().unwrap();
let frame_rate = core_lock.frame_rate();
core_lock.audio_mut().set_frame_rate(frame_rate);
drop(core_lock);
// Create instance. // Create instance.
let instance = RuffleInstance { let instance = RuffleInstance {

View File

@ -2,9 +2,10 @@
use js_sys::{Array, ArrayBuffer, Uint8Array}; use js_sys::{Array, ArrayBuffer, Uint8Array};
use ruffle_core::backend::navigator::{ use ruffle_core::backend::navigator::{
NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions, url_from_relative_url, NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions,
}; };
use ruffle_core::loader::Error; use ruffle_core::loader::Error;
use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
@ -158,4 +159,17 @@ impl NavigatorBackend for WebNavigatorBackend {
} }
}) })
} }
fn resolve_relative_url<'a>(&mut self, url: &'a str) -> Cow<'a, str> {
let window = web_sys::window().expect("window()");
let document = window.document().expect("document()");
if let Ok(Some(base_uri)) = document.base_uri() {
if let Ok(new_url) = url_from_relative_url(&base_uri, url) {
return new_url.into_string().into();
}
}
url.into()
}
} }