diff --git a/core/src/config.rs b/core/src/config.rs index 36ad65e09..30fdd3eeb 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; /// /// When letterboxed, black bars will be rendered around the exterior /// margins of the content. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Collect, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Collect, Serialize, Deserialize)] #[collect(require_static)] #[serde(rename = "letterbox")] pub enum Letterbox { @@ -16,7 +16,6 @@ pub enum Letterbox { /// The content will only be letterboxed if the content is running fullscreen. #[serde(rename = "fullscreen")] - #[default] Fullscreen, /// The content will always be letterboxed. diff --git a/web/packages/core/src/config.ts b/web/packages/core/src/config.ts index 5b9202330..19521b109 100644 --- a/web/packages/core/src/config.ts +++ b/web/packages/core/src/config.ts @@ -1,7 +1,11 @@ -/** - * Represents the various types of auto-play behaviours that are supported. - */ import type { BaseLoadOptions } from "./load-options"; +import { + AutoPlay, + UnmuteOverlay, + WindowMode, + Letterbox, + LogLevel, +} from "./load-options"; /** * The configuration object to control Ruffle's behaviour on the website @@ -10,8 +14,10 @@ import type { BaseLoadOptions } from "./load-options"; export interface Config extends BaseLoadOptions { /** * The URL at which Ruffle can load its extra files (i.e. `.wasm`). + * + * @default null */ - publicPath?: string; + publicPath?: string | null; /** * Whether or not to enable polyfills on the page. @@ -24,3 +30,27 @@ export interface Config extends BaseLoadOptions { */ polyfills?: boolean; } + +export const DEFAULT_CONFIG: Required = { + allowScriptAccess: false, + parameters: {}, + autoplay: AutoPlay.Auto, + backgroundColor: null, + letterbox: Letterbox.Fullscreen, + unmuteOverlay: UnmuteOverlay.Visible, + upgradeToHttps: true, + warnOnUnsupportedContent: true, + logLevel: LogLevel.Error, + showSwfDownload: false, + contextMenu: true, + preloader: true, + maxExecutionDuration: { secs: 15, nanos: 0 }, + base: null, + menu: true, + salign: "", + quality: "high", + scale: "showAll", + wmode: WindowMode.Opaque, + publicPath: null, + polyfills: true, +}; diff --git a/web/packages/core/src/load-options.ts b/web/packages/core/src/load-options.ts index 7302b21dd..22317a6ef 100644 --- a/web/packages/core/src/load-options.ts +++ b/web/packages/core/src/load-options.ts @@ -1,3 +1,6 @@ +/** + * Represents the various types of auto-play behaviours that are supported. + */ export const enum AutoPlay { /** * The player should automatically play the movie as soon as it is loaded. @@ -135,6 +138,8 @@ export interface BaseLoadOptions { * If a URL if specified when loading the movie, some parameters will * be extracted by the query portion of that URL and then overwritten * by any explicitly set here. + * + * @default {} */ parameters?: URLSearchParams | string | Record; @@ -225,7 +230,7 @@ export interface BaseLoadOptions { * Maximum amount of time a script can take before scripting * is disabled. * - * @default {"secs": 15, "nanos": 0} + * @default { secs: 15, nanos: 0 } */ maxExecutionDuration?: { secs: number; @@ -250,7 +255,6 @@ export interface BaseLoadOptions { menu?: boolean; /** - * * This is equivalent to Stage.align. * * @default "" @@ -258,7 +262,6 @@ export interface BaseLoadOptions { salign?: string; /** - * * This is equivalent to Stage.quality. * * @default "high" @@ -266,7 +269,6 @@ export interface BaseLoadOptions { quality?: string; /** - * * This is equivalent to Stage.scaleMode. * * @default "showAll" diff --git a/web/packages/core/src/public-path.ts b/web/packages/core/src/public-path.ts index 3c4e06f94..cd99086b8 100644 --- a/web/packages/core/src/public-path.ts +++ b/web/packages/core/src/public-path.ts @@ -44,7 +44,7 @@ try { export function publicPath(config: Config): string { // Default to the directory where this script resides. let path = currentScriptURL; - if (config !== undefined && config.publicPath !== undefined) { + if (config.publicPath !== null && config.publicPath !== undefined) { path = config.publicPath; } diff --git a/web/packages/core/src/ruffle-player.ts b/web/packages/core/src/ruffle-player.ts index 6854dd0bf..12e3c224f 100644 --- a/web/packages/core/src/ruffle-player.ts +++ b/web/packages/core/src/ruffle-player.ts @@ -1,11 +1,10 @@ import type { Ruffle } from "../pkg/ruffle_web"; - import { loadRuffle } from "./load-ruffle"; import { ruffleShadowTemplate } from "./shadow-template"; import { lookupElement } from "./register-element"; import type { Config } from "./config"; +import { DEFAULT_CONFIG } from "./config"; import { - BaseLoadOptions, DataLoadOptions, URLLoadOptions, AutoPlay, @@ -127,24 +126,21 @@ export class RufflePlayer extends HTMLElement { // Firefox has a read-only "contextMenu" property, // so avoid shadowing it. private contextMenuElement: HTMLElement; - private hasContextMenu = false; // Allows the user to permanently disable the context menu. private contextMenuForceDisabled = false; - // Whether to show a preloader while Ruffle is still loading the SWF - private hasPreloader = true; - // Whether this device is a touch device. // Set to true when a touch event is encountered. private isTouch = false; + // The effective config loaded upon `.load()`. + private loadedConfig: Required = DEFAULT_CONFIG; + private swfUrl?: URL; private instance: Ruffle | null; - private options: BaseLoadOptions | null; private lastActivePlayingState: boolean; - private showSwfDownload = false; private _metadata: MovieMetadata | null; private _readyState: ReadyState; @@ -237,7 +233,6 @@ export class RufflePlayer extends HTMLElement { window.addEventListener("click", this.hideContextMenu.bind(this)); this.instance = null; - this.options = null; this.onFSCommand = null; this._readyState = ReadyState.HaveNothing; @@ -395,14 +390,14 @@ export class RufflePlayer extends HTMLElement { * * @private */ - private async ensureFreshInstance(config: BaseLoadOptions): Promise { + private async ensureFreshInstance(): Promise { this.destroy(); - if (this.hasPreloader) { + if (this.loadedConfig.preloader !== false) { this.showPreloader(); } const ruffleConstructor = await loadRuffle( - config, + this.loadedConfig, this.onRuffleDownloadProgress.bind(this) ).catch((e) => { console.error(`Serious error loading Ruffle: ${e}`); @@ -446,7 +441,7 @@ export class RufflePlayer extends HTMLElement { this.instance = await new ruffleConstructor( this.container, this, - config + this.loadedConfig ); console.log( "New Ruffle instance created (WebAssembly extensions: " + @@ -469,17 +464,17 @@ export class RufflePlayer extends HTMLElement { this.unmuteAudioContext(); - // Treat unspecified and invalid values as `AutoPlay.Auto`. + // Treat invalid values as `AutoPlay.Auto`. if ( - config.autoplay === AutoPlay.On || - (config.autoplay !== AutoPlay.Off && + this.loadedConfig.autoplay === AutoPlay.On || + (this.loadedConfig.autoplay !== AutoPlay.Off && this.audioState() === "running") ) { this.play(); if (this.audioState() !== "running") { - // Treat unspecified and invalid values as `UnmuteOverlay.Visible`. - if (config.unmuteOverlay !== UnmuteOverlay.Hidden) { + // Treat invalid values as `UnmuteOverlay.Visible`. + if (this.loadedConfig.unmuteOverlay !== UnmuteOverlay.Hidden) { this.unmuteOverlay.style.display = "block"; } @@ -541,6 +536,39 @@ export class RufflePlayer extends HTMLElement { } } + private checkOptions( + options: string | URLLoadOptions | DataLoadOptions + ): URLLoadOptions | DataLoadOptions { + if (typeof options === "string") { + return { url: options }; + } + + const check: ( + condition: boolean, + message: string + ) => asserts condition = (condition, message) => { + if (!condition) { + const error = new TypeError(message); + error.ruffleIndexError = PanicError.JavascriptConfiguration; + this.panic(error); + throw error; + } + }; + check( + options !== null && typeof options === "object", + "Argument 0 must be a string or object" + ); + check( + "url" in options || "data" in options, + "Argument 0 must contain a `url` or `data` key" + ); + check( + !("url" in options) || typeof options.url === "string", + "`url` must be a string" + ); + return options; + } + /** * Loads a specified movie into this player. * @@ -557,34 +585,7 @@ export class RufflePlayer extends HTMLElement { async load( options: string | URLLoadOptions | DataLoadOptions ): Promise { - let optionsError = ""; - switch (typeof options) { - case "string": - options = { url: options }; - break; - case "object": - if (options === null) { - optionsError = "Argument 0 must be a string or object"; - } else if (!("url" in options) && !("data" in options)) { - optionsError = - "Argument 0 must contain a `url` or `data` key"; - } else if ( - "url" in options && - typeof options.url !== "string" - ) { - optionsError = "`url` must be a string"; - } - break; - default: - optionsError = "Argument 0 must be a string or object"; - break; - } - if (optionsError.length > 0) { - const error = new TypeError(optionsError); - error.ruffleIndexError = PanicError.JavascriptConfiguration; - this.panic(error); - throw error; - } + options = this.checkOptions(options); if (!this.isConnected || this.isUnusedFallbackObject()) { console.warn( @@ -599,28 +600,27 @@ export class RufflePlayer extends HTMLElement { } try { - const config: BaseLoadOptions = { + this.loadedConfig = { + ...DEFAULT_CONFIG, ...(window.RufflePlayer?.config ?? {}), ...this.config, ...options, }; - // `allowScriptAccess` can only be set in `options`. - config.allowScriptAccess = options.allowScriptAccess; - this.showSwfDownload = config.showSwfDownload === true; - this.options = options; - this.hasContextMenu = config.contextMenu !== false; - this.hasPreloader = config.preloader !== false; + // `allowScriptAccess` can only be set in `options`. + this.loadedConfig.allowScriptAccess = + options.allowScriptAccess === true; // Pre-emptively set background color of container while Ruffle/SWF loads. if ( - config.backgroundColor && - config.wmode !== WindowMode.Transparent + this.loadedConfig.backgroundColor && + this.loadedConfig.wmode !== WindowMode.Transparent ) { - this.container.style.backgroundColor = config.backgroundColor; + this.container.style.backgroundColor = + this.loadedConfig.backgroundColor; } - await this.ensureFreshInstance(config); + await this.ensureFreshInstance(); if ("url" in options) { console.log(`Loading SWF file ${options.url}`); @@ -844,7 +844,11 @@ export class RufflePlayer extends HTMLElement { } } - if (this.instance && this.swfUrl && this.showSwfDownload) { + if ( + this.instance && + this.swfUrl && + this.loadedConfig.showSwfDownload === true + ) { items.push(null); items.push({ text: "Download .swf", @@ -882,7 +886,10 @@ export class RufflePlayer extends HTMLElement { private showContextMenu(e: MouseEvent): void { e.preventDefault(); - if (!this.hasContextMenu || this.contextMenuForceDisabled) { + if ( + this.loadedConfig.contextMenu === false || + this.contextMenuForceDisabled + ) { return; } @@ -1515,9 +1522,7 @@ export class RufflePlayer extends HTMLElement { } protected debugPlayerInfo(): string { - let result = `Allows script access: ${ - this.options?.allowScriptAccess ?? false - }\n`; + let result = `Allows script access: ${this.loadedConfig.allowScriptAccess}\n`; if (this.instance) { result += `Renderer: ${this.instance.renderer_name()}\n`; } diff --git a/web/src/lib.rs b/web/src/lib.rs index 61db47d45..6d157f2bb 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -117,8 +117,7 @@ struct JavascriptInterface { } #[derive(Serialize, Deserialize)] -#[serde(default = "Default::default")] -pub struct Config { +struct Config { #[serde(rename = "allowScriptAccess")] allow_script_access: bool, @@ -154,26 +153,6 @@ pub struct Config { max_execution_duration: Duration, } -impl Default for Config { - fn default() -> Self { - Self { - allow_script_access: false, - show_menu: true, - salign: Some("".to_owned()), - quality: Some("high".to_owned()), - scale: Some("showAll".to_owned()), - wmode: Some("opaque".to_owned()), - background_color: Default::default(), - letterbox: Default::default(), - upgrade_to_https: true, - base_url: None, - warn_on_unsupported_content: true, - log_level: log::Level::Error, - max_execution_duration: Duration::from_secs(15), - } - } -} - /// Metadata about the playing SWF file to be passed back to JavaScript. #[derive(Serialize)] struct MovieMetadata { @@ -205,8 +184,10 @@ impl Ruffle { #[allow(clippy::new_ret_no_self)] #[wasm_bindgen(constructor)] pub fn new(parent: HtmlElement, js_player: JavascriptPlayer, config: JsValue) -> Promise { - let config: Config = serde_wasm_bindgen::from_value(config).unwrap_or_default(); wasm_bindgen_futures::future_to_promise(async move { + let config: Config = serde_wasm_bindgen::from_value(config) + .map_err(|e| format!("Error parsing config: {}", e))?; + if RUFFLE_GLOBAL_PANIC.is_completed() { // If an actual panic happened, then we can't trust the state it left us in. // Prevent future players from loading so that they can inform the user about the error.