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

View File

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

View File

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

View File

@ -898,11 +898,15 @@ fn drop_target<'gc>(
fn url<'gc>(
_activation: &mut Activation<'_, 'gc>,
_context: &mut UpdateContext<'_, 'gc, '_>,
_this: DisplayObject<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: DisplayObject<'gc>,
) -> Result<Value<'gc>, Error<'gc>> {
log::warn!("Unimplemented property _url");
Ok("".into())
Ok(this
.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>(

View File

@ -1,6 +1,7 @@
//! Browser-related platform functions
use crate::loader::Error;
use std::borrow::Cow;
use std::collections::{HashMap, VecDeque};
use std::fs;
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::time::Duration;
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.
#[derive(Copy, Clone)]
@ -132,6 +184,16 @@ pub trait NavigatorBackend {
/// TODO: For some reason, `wasm_bindgen_futures` wants unpinnable futures.
/// This seems highly limiting.
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.
@ -303,4 +365,13 @@ impl NavigatorBackend for NullNavigatorBackend {
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 {
let mut actions = vec![];
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 {
let button_action = ButtonAction {
action_data: action_data.clone(),

View File

@ -2567,7 +2567,7 @@ impl ClipAction {
let len = other.action_data.len();
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 {
event: match event {
ClipEventFlag::Construct => ClipEvent::Construct,

View File

@ -424,6 +424,7 @@ mod tests {
use crate::font::{EvalParameters, Font};
use crate::player::{Player, DEVICE_FONT_TAG};
use gc_arena::{rootless_arena, MutationContext};
use std::ops::DerefMut;
use swf::Twips;
fn with_device_font<F>(callback: F)
@ -432,7 +433,8 @@ mod tests {
{
rootless_arena(|mc| {
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);
})

View File

@ -22,6 +22,9 @@ pub enum Error {
#[error("Load cancelled")]
Cancelled,
#[error("Non-root-movie loader spawned as root movie loader")]
NotRootMovieLoader,
#[error("Non-movie loader spawned as movie loader")]
NotMovieLoader,
@ -34,6 +37,9 @@ pub enum Error {
#[error("Non-XML loader spawned as XML loader")]
NotXmlLoader,
#[error("Could not fetch movie {0}")]
FetchError(String),
#[error("Invalid SWF")]
InvalidSwf(#[from] crate::tag_utils::Error),
@ -108,6 +114,27 @@ impl<'gc> LoadManager<'gc> {
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.
///
/// Returns the loader's async process, which you will need to spawn.
@ -116,6 +143,7 @@ impl<'gc> LoadManager<'gc> {
player: Weak<Mutex<Player>>,
target_clip: DisplayObject<'gc>,
fetch: OwnedFuture<Vec<u8>, Error>,
url: String,
target_broadcaster: Option<Object<'gc>>,
) -> OwnedFuture<(), Error> {
let loader = Loader::Movie {
@ -129,7 +157,7 @@ impl<'gc> LoadManager<'gc> {
let loader = self.get_loader_mut(handle).unwrap();
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).
@ -229,6 +257,12 @@ impl<'gc> Default for LoadManager<'gc> {
/// A struct that holds garbage-collected pointers for asynchronous code.
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.
Movie {
/// The handle to refer to this loader instance.
@ -291,6 +325,7 @@ pub enum Loader<'gc> {
unsafe impl<'gc> Collect for Loader<'gc> {
fn trace(&self, cc: CollectionContext) {
match self {
Loader::RootMovie { .. } => {}
Loader::Movie {
target_clip,
target_broadcaster,
@ -313,6 +348,7 @@ impl<'gc> Loader<'gc> {
/// run.
pub fn introduce_loader_handle(&mut self, handle: Handle) {
match self {
Loader::RootMovie { self_handle, .. } => *self_handle = Some(handle),
Loader::Movie { self_handle, .. } => *self_handle = Some(handle),
Loader::Form { 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.
///
/// The given future should be passed immediately to an executor; it will
@ -331,6 +407,7 @@ impl<'gc> Loader<'gc> {
&mut self,
player: Weak<Mutex<Player>>,
fetch: OwnedFuture<Vec<u8>, Error>,
mut url: String,
) -> OwnedFuture<(), Error> {
let handle = match self {
Loader::Movie { self_handle, .. } => self_handle.expect("Loader not self-introduced"),
@ -344,6 +421,8 @@ impl<'gc> Loader<'gc> {
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();
let (clip, broadcaster) = match uc.load_manager.get_loader(handle) {
Some(Loader::Movie {
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 {
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::avm2::Avm2;
use crate::backend::input::{InputBackend, MouseCursor};
use crate::backend::navigator::{NavigatorBackend, RequestOptions};
use crate::backend::storage::StorageBackend;
use crate::backend::{
audio::AudioBackend, navigator::NavigatorBackend, render::Letterbox, render::RenderBackend,
};
use crate::backend::{audio::AudioBackend, render::Letterbox, render::RenderBackend};
use crate::context::{ActionQueue, ActionType, RenderContext, UpdateContext};
use crate::display_object::{EditText, MorphShape, MovieClip};
use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode, PlayerEvent};
@ -183,29 +182,21 @@ pub struct Player {
impl Player {
pub fn new(
mut renderer: Renderer,
renderer: Renderer,
audio: Audio,
navigator: Navigator,
input: Input,
movie: SwfMovie,
storage: Storage,
) -> Result<Arc<Mutex<Self>>, Error> {
let movie = Arc::new(movie);
info!(
"Loaded SWF version {}, with a resolution of {}x{}",
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 fake_movie = Arc::new(SwfMovie::empty(NEWEST_PLAYER_VERSION));
let movie_width = 550;
let movie_height = 400;
let frame_rate = 12.0;
let mut player = Player {
player_version: NEWEST_PLAYER_VERSION,
swf: movie.clone(),
swf: fake_movie,
is_playing: false,
needs_render: true,
@ -223,26 +214,10 @@ impl Player {
rng: SmallRng::from_seed([0u8; 16]), // TODO(Herschel): Get a proper seed on all platforms.
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(
gc_context,
GcRootData {
library,
library: Library::default(),
levels: BTreeMap::new(),
mouse_hovered_object: None,
drag_object: None,
@ -257,7 +232,7 @@ impl Player {
))
}),
frame_rate: movie.header().frame_rate.into(),
frame_rate,
frame_accumulator: 0.0,
movie_width,
@ -281,14 +256,77 @@ impl Player {
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 =
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.post_instantiation(avm1, context, root, None, false);
root.set_name(context.gc_context, "");
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(
avm1,
ActivationIdentifier::root("[Version Setter]"),
@ -310,15 +348,9 @@ impl Player {
);
});
player.build_matrices();
player.preload();
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)
self.build_matrices();
self.preload();
self.audio.set_frame_rate(self.frame_rate);
}
pub fn tick(&mut self, dt: f64) {
@ -1066,14 +1098,11 @@ impl Player {
pub fn load_device_font<'gc>(
gc_context: gc_arena::MutationContext<'gc, '_>,
data: &[u8],
renderer: &mut Renderer,
renderer: &mut dyn RenderBackend,
) -> Result<crate::font::Font<'gc>, Error> {
let mut reader = swf::read::Reader::new(data, 8);
let device_font = crate::font::Font::from_swf_tag(
gc_context,
renderer.deref_mut(),
&reader.read_define_font_2(3)?,
)?;
let device_font =
crate::font::Font::from_swf_tag(gc_context, renderer, &reader.read_define_font_2(3)?)?;
Ok(device_font)
}

View File

@ -1,3 +1,4 @@
use crate::backend::navigator::url_from_relative_path;
use gc_arena::Collect;
use std::path::Path;
use std::sync::Arc;
@ -17,6 +18,9 @@ pub struct SwfMovie {
/// Uncompressed SWF data.
data: Vec<u8>,
/// The URL the SWF was downloaded from.
url: Option<String>,
}
impl SwfMovie {
@ -31,25 +35,37 @@ impl SwfMovie {
num_frames: 0,
},
data: vec![],
url: None,
}
}
/// Construct a movie from an existing movie with any particular data on it.
pub fn from_movie_and_subdata(&self, data: Vec<u8>) -> Self {
/// Construct a movie from an existing movie with any particular data on
/// 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 {
header: self.header.clone(),
data,
url: source.url.clone(),
}
}
/// Utility method to construct a movie from a file on disk.
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)?;
Self::from_data(&data)
Self::from_data(&data, Some(url))
}
/// 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 header = swf_stream.header;
let mut reader = swf_stream.reader;
@ -74,7 +90,7 @@ impl SwfMovie {
data
};
Ok(Self { header, data })
Ok(Self { header, data, url })
}
pub fn header(&self) -> &Header {
@ -97,6 +113,11 @@ impl SwfMovie {
pub fn height(&self) -> 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.
@ -141,12 +162,12 @@ impl SwfSlice {
/// Construct a new slice with a given dataset only.
///
/// This is used primarily for converting owned data back into a slice: we
/// reattach the SWF data that we can
pub fn owned_subslice(&self, data: Vec<u8>) -> Self {
/// reattach the SWF data to a fresh movie and return a new slice into it.
pub fn owned_subslice(&self, data: Vec<u8>, source: &SwfMovie) -> Self {
let len = data.len();
Self {
movie: Arc::new(self.movie.from_movie_and_subdata(data)),
movie: Arc::new(self.movie.from_movie_and_subdata(data, source)),
start: 0,
end: len,
}

View File

@ -13,6 +13,7 @@ use ruffle_core::tag_utils::SwfMovie;
use ruffle_core::Player;
use std::cell::RefCell;
use std::path::Path;
use std::sync::Arc;
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(NullNavigatorBackend::with_base_path(base_path, channel)),
Box::new(NullInputBackend::new()),
movie,
Box::new(MemoryStorageBackend::default()),
)?;
player.lock().unwrap().set_root_movie(Arc::new(movie));
for _ in 0..num_frames {
player.lock().unwrap().run_frame();

View File

@ -16,6 +16,7 @@ use ruffle_core::{
};
use ruffle_render_wgpu::WgpuRenderBackend;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
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(
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

View File

@ -2,9 +2,10 @@
use crate::custom_event::RuffleEvent;
use ruffle_core::backend::navigator::{
NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions,
url_from_relative_path, NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions,
};
use ruffle_core::loader::Error;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs;
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::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use structopt::StructOpt;
use walkdir::{DirEntry, WalkDir};
@ -84,7 +85,6 @@ fn take_screenshot(
Box::new(NullAudioBackend::new()),
Box::new(NullNavigatorBackend::new()),
Box::new(NullInputBackend::new()),
movie,
Box::new(MemoryStorageBackend::default()),
)?;
@ -92,6 +92,7 @@ fn take_screenshot(
.lock()
.unwrap()
.set_viewport_dimensions(width, height);
player.lock().unwrap().set_root_movie(Arc::new(movie));
let mut result = Vec::new();
let totalframes = frames + skipframes;

View File

@ -116,28 +116,48 @@ exports.RufflePlayer = class RufflePlayer extends HTMLElement {
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) {
//TODO: Actually stream files...
try {
if (this.isConnected && !this.is_unused_fallback_object()) {
let abs_url = new URL(url, window.location.href).toString();
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) {
let data = await response.arrayBuffer();
await this.play_swf_data(data);
console.log("Playing " + url);
} else {
console.error(
"SWF load failed: " +
response.status +
" " +
response.statusText +
" for " +
url
);
if (this.play_button) {
this.play_button.style.display = "block";
}
} else {
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) {
if (this.isConnected && !this.is_unused_fallback_object()) {
console.log("Got SWF data");
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, new Uint8Array(data));
await this.ensure_fresh_instance();
this.instance.load_data(new Uint8Array(data));
console.log("New Ruffle instance created.");
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::PlayerEvent;
use ruffle_web_common::JsResult;
use std::mem::drop;
use std::sync::{Arc, Mutex};
use std::{cell::RefCell, error::Error, num::NonZeroI32};
use wasm_bindgen::{prelude::*, JsCast, JsValue};
@ -57,8 +56,38 @@ pub struct Ruffle(Index);
#[wasm_bindgen]
impl Ruffle {
pub fn new(parent: HtmlElement, swf_data: Uint8Array) -> Result<Ruffle, JsValue> {
Ruffle::new_internal(parent, swf_data).map_err(|_| "Error creating player".into())
pub fn new(parent: HtmlElement) -> Result<Ruffle, JsValue> {
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) {
@ -106,16 +135,10 @@ 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();
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 document = window.document().ok_or("Expected document")?;
@ -138,12 +161,7 @@ impl Ruffle {
})
.unwrap_or_else(|| Box::new(MemoryStorageBackend::default()));
let core =
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);
let core = ruffle_core::Player::new(renderer, audio, navigator, input, local_storage)?;
// Create instance.
let instance = RuffleInstance {

View File

@ -2,9 +2,10 @@
use js_sys::{Array, ArrayBuffer, Uint8Array};
use ruffle_core::backend::navigator::{
NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions,
url_from_relative_url, NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions,
};
use ruffle_core::loader::Error;
use std::borrow::Cow;
use std::collections::HashMap;
use std::time::Duration;
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()
}
}