web: Don't parse config in Rust, do it in Typescript with some tests
This commit is contained in:
parent
6e53f98068
commit
3fa8735e97
|
@ -41,7 +41,7 @@ impl FromStr for Letterbox {
|
|||
|
||||
/// The networking API access mode of the Ruffle player.
|
||||
/// This setting is only used on web.
|
||||
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum NetworkingAccessMode {
|
||||
/// All networking APIs are permitted in the SWF file.
|
||||
#[serde(rename = "all")]
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
import type { RuffleInstanceBuilder } from "../../dist/ruffle_web";
|
||||
import { BaseLoadOptions, Duration, SecsDuration } from "../load-options";
|
||||
|
||||
/**
|
||||
* Checks if the given value is explicitly `T` (not null, not undefined)
|
||||
*
|
||||
* @param value The value to test
|
||||
* @returns true if the value isn't null or undefined
|
||||
*/
|
||||
function isExplicit<T>(value: T | undefined | null): value is T {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the given RuffleInstanceBuilder for the general options provided.
|
||||
*
|
||||
* This is the translation layer between what we allow users to provide through e.g. `window.RufflePlayer.config`,
|
||||
* which is quite relaxed and may evolve over time,
|
||||
* and the actual values we accept inside Rust (which is quite strict).
|
||||
*
|
||||
* This allows us to change the rust side at will, and without needing to worry about backwards compatibility, parsing, etc.
|
||||
*
|
||||
* @param builder The builder to set the options on
|
||||
* @param config The options to apply
|
||||
*/
|
||||
export function configureBuilder(
|
||||
builder: RuffleInstanceBuilder,
|
||||
config: BaseLoadOptions,
|
||||
) {
|
||||
// Guard things for being explicitly set, so that we don't need to specify defaults in yet another place...
|
||||
|
||||
if (isExplicit(config.allowScriptAccess)) {
|
||||
builder.setAllowScriptAccess(config.allowScriptAccess);
|
||||
}
|
||||
if (isExplicit(config.backgroundColor)) {
|
||||
builder.setBackgroundColor(parseColor(config.backgroundColor));
|
||||
}
|
||||
if (isExplicit(config.upgradeToHttps)) {
|
||||
builder.setUpgradeToHttps(config.upgradeToHttps);
|
||||
}
|
||||
if (isExplicit(config.compatibilityRules)) {
|
||||
builder.setCompatibilityRules(config.compatibilityRules);
|
||||
}
|
||||
if (isExplicit(config.letterbox)) {
|
||||
builder.setLetterbox(config.letterbox.toLowerCase());
|
||||
}
|
||||
if (isExplicit(config.base)) {
|
||||
builder.setBaseUrl(config.base);
|
||||
}
|
||||
if (isExplicit(config.menu)) {
|
||||
builder.setShowMenu(config.menu);
|
||||
}
|
||||
if (isExplicit(config.allowFullscreen)) {
|
||||
builder.setAllowFullscreen(config.allowFullscreen);
|
||||
}
|
||||
if (isExplicit(config.salign)) {
|
||||
builder.setStageAlign(config.salign.toLowerCase());
|
||||
}
|
||||
if (isExplicit(config.forceAlign)) {
|
||||
builder.setForceAlign(config.forceAlign);
|
||||
}
|
||||
if (isExplicit(config.quality)) {
|
||||
builder.setQuality(config.quality.toLowerCase());
|
||||
}
|
||||
if (isExplicit(config.scale)) {
|
||||
builder.setScale(config.scale.toLowerCase());
|
||||
}
|
||||
if (isExplicit(config.forceScale)) {
|
||||
builder.setForceScale(config.forceScale);
|
||||
}
|
||||
if (isExplicit(config.frameRate)) {
|
||||
builder.setFrameRate(config.frameRate);
|
||||
}
|
||||
if (isExplicit(config.wmode)) {
|
||||
builder.setWmode(config.wmode);
|
||||
}
|
||||
if (isExplicit(config.logLevel)) {
|
||||
builder.setLogLevel(config.logLevel);
|
||||
}
|
||||
if (isExplicit(config.maxExecutionDuration)) {
|
||||
builder.setMaxExecutionDuration(
|
||||
parseDuration(config.maxExecutionDuration),
|
||||
);
|
||||
}
|
||||
if (isExplicit(config.playerVersion)) {
|
||||
builder.setPlayerVersion(config.playerVersion);
|
||||
}
|
||||
if (isExplicit(config.preferredRenderer)) {
|
||||
builder.setPreferredRenderer(config.preferredRenderer);
|
||||
}
|
||||
if (isExplicit(config.openUrlMode)) {
|
||||
builder.setOpenUrlMode(config.openUrlMode.toLowerCase());
|
||||
}
|
||||
if (isExplicit(config.allowNetworking)) {
|
||||
builder.setAllowNetworking(config.allowNetworking.toLowerCase());
|
||||
}
|
||||
if (isExplicit(config.credentialAllowList)) {
|
||||
builder.setCredentialAllowList(config.credentialAllowList);
|
||||
}
|
||||
if (isExplicit(config.playerRuntime)) {
|
||||
builder.setPlayerRuntime(config.playerRuntime);
|
||||
}
|
||||
|
||||
if (isExplicit(config.socketProxy)) {
|
||||
for (const proxy of config.socketProxy) {
|
||||
builder.addSocketProxy(proxy.host, proxy.port, proxy.proxyUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a color into an RGB value.
|
||||
*
|
||||
* @param color The color string to parse
|
||||
* @returns A valid RGB number, or undefined if invalid
|
||||
*/
|
||||
export function parseColor(color: string): number | undefined {
|
||||
if (color.startsWith("#")) {
|
||||
color = color.substring(1);
|
||||
}
|
||||
if (color.length < 6) {
|
||||
return undefined;
|
||||
}
|
||||
let result = 0;
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const digit = parseInt(color[i]!, 16);
|
||||
if (!isNaN(digit)) {
|
||||
result = (result << 4) | digit;
|
||||
} else {
|
||||
result = result << 4;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a duration into number of seconds.
|
||||
*
|
||||
* @param value The duration to parse
|
||||
* @returns A valid number of seconds
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function parseDuration(value: Duration): SecsDuration {
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
return value.secs;
|
||||
}
|
|
@ -9,11 +9,10 @@ import {
|
|||
signExtensions,
|
||||
referenceTypes,
|
||||
} from "wasm-feature-detect";
|
||||
import type { RuffleHandle } from "../dist/ruffle_web";
|
||||
import type { RuffleInstanceBuilder } from "../dist/ruffle_web";
|
||||
import { setPolyfillsOnLoad } from "./js-polyfills";
|
||||
import { publicPath } from "./public-path";
|
||||
import { BaseLoadOptions } from "./load-options";
|
||||
import { RufflePlayer } from "./ruffle-player";
|
||||
|
||||
declare global {
|
||||
let __webpack_public_path__: string;
|
||||
|
@ -30,13 +29,13 @@ type ProgressCallback = (bytesLoaded: number, bytesTotal: number) => void;
|
|||
*
|
||||
* @param config The `window.RufflePlayer.config` object.
|
||||
* @param progressCallback The callback that will be run with Ruffle's download progress.
|
||||
* @returns A ruffle constructor that may be used to create new Ruffle
|
||||
* @returns A ruffle-builder constructor that may be used to create new RuffleInstanceBuilder
|
||||
* instances.
|
||||
*/
|
||||
async function fetchRuffle(
|
||||
config: BaseLoadOptions,
|
||||
progressCallback?: ProgressCallback,
|
||||
): Promise<typeof RuffleHandle> {
|
||||
): Promise<typeof RuffleInstanceBuilder> {
|
||||
// Apply some pure JavaScript polyfills to prevent conflicts with external
|
||||
// libraries, if needed.
|
||||
setPolyfillsOnLoad();
|
||||
|
@ -66,7 +65,7 @@ async function fetchRuffle(
|
|||
|
||||
// Note: The argument passed to import() has to be a simple string literal,
|
||||
// otherwise some bundler will get confused and won't include the module?
|
||||
const { default: init, RuffleHandle } = await (extensionsSupported
|
||||
const { default: init, RuffleInstanceBuilder } = await (extensionsSupported
|
||||
? import("../dist/ruffle_web-wasm_extensions")
|
||||
: import("../dist/ruffle_web"));
|
||||
let response;
|
||||
|
@ -114,32 +113,28 @@ async function fetchRuffle(
|
|||
|
||||
await init(response);
|
||||
|
||||
return RuffleHandle;
|
||||
return RuffleInstanceBuilder;
|
||||
}
|
||||
|
||||
let nativeConstructor: Promise<typeof RuffleHandle> | null = null;
|
||||
let nativeConstructor: Promise<typeof RuffleInstanceBuilder> | null = null;
|
||||
|
||||
/**
|
||||
* Obtain an instance of `Ruffle`.
|
||||
*
|
||||
* This function returns a promise which yields `Ruffle` asynchronously.
|
||||
* This function returns a promise which yields a new `RuffleInstanceBuilder` asynchronously.
|
||||
*
|
||||
* @param container The container that the resulting canvas will be added to.
|
||||
* @param player The `RufflePlayer` object responsible for this instance of Ruffle.
|
||||
* @param config The `window.RufflePlayer.config` object.
|
||||
* @param progressCallback The callback that will be run with Ruffle's download progress.
|
||||
* @returns A ruffle instance.
|
||||
* @returns A ruffle instance builder.
|
||||
*/
|
||||
export async function loadRuffle(
|
||||
container: HTMLElement,
|
||||
player: RufflePlayer,
|
||||
export async function createRuffleBuilder(
|
||||
config: BaseLoadOptions,
|
||||
progressCallback?: ProgressCallback,
|
||||
): Promise<RuffleHandle> {
|
||||
): Promise<RuffleInstanceBuilder> {
|
||||
if (nativeConstructor === null) {
|
||||
nativeConstructor = fetchRuffle(config, progressCallback);
|
||||
}
|
||||
|
||||
const constructor = await nativeConstructor;
|
||||
return new constructor(container, player, config);
|
||||
return new constructor();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { RuffleHandle } from "../dist/ruffle_web";
|
||||
import { loadRuffle } from "./load-ruffle";
|
||||
import { createRuffleBuilder } from "./load-ruffle";
|
||||
import { applyStaticStyles, ruffleShadowTemplate } from "./shadow-template";
|
||||
import { lookupElement } from "./register-element";
|
||||
import { DEFAULT_CONFIG } from "./config";
|
||||
|
@ -17,6 +17,7 @@ import { buildInfo } from "./build-info";
|
|||
import { text, textAsParagraphs } from "./i18n";
|
||||
import JSZip from "jszip";
|
||||
import { isExtension } from "./current-script";
|
||||
import { configureBuilder } from "./internal/builder";
|
||||
|
||||
const RUFFLE_ORIGIN = "https://ruffle.rs";
|
||||
const DIMENSION_REGEX = /^\s*(\d+(\.\d+)?(%)?)/;
|
||||
|
@ -672,9 +673,7 @@ export class RufflePlayer extends HTMLElement {
|
|||
'The configuration option contextMenu no longer takes a boolean. Use "on", "off", or "rightClickOnly".',
|
||||
);
|
||||
}
|
||||
this.instance = await loadRuffle(
|
||||
this.container,
|
||||
this,
|
||||
const builder = await createRuffleBuilder(
|
||||
this.loadedConfig || {},
|
||||
this.onRuffleDownloadProgress.bind(this),
|
||||
).catch((e) => {
|
||||
|
@ -715,6 +714,12 @@ export class RufflePlayer extends HTMLElement {
|
|||
this.panic(e);
|
||||
throw e;
|
||||
});
|
||||
configureBuilder(builder, this.loadedConfig || {});
|
||||
this.instance = await builder.build(this.container, this).catch((e) => {
|
||||
console.error(`Serious error loading Ruffle: ${e}`);
|
||||
this.panic(e);
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (this.loadedConfig?.fontSources) {
|
||||
for (const url of this.loadedConfig.fontSources) {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { strict as assert } from "assert";
|
||||
import { parseColor, parseDuration } from "../src/internal/builder";
|
||||
|
||||
describe("Color parsing", function () {
|
||||
it("should parse a valid RRGGBB hex, with hash", function () {
|
||||
assert.strictEqual(parseColor("#A1B2C3"), 0xa1b2c3);
|
||||
});
|
||||
|
||||
it("should parse a valid RRGGBB hex, without hash", function () {
|
||||
assert.strictEqual(parseColor("#1A2B3C"), 0x1a2b3c);
|
||||
});
|
||||
|
||||
it("should fail with not enough digits", function () {
|
||||
assert.strictEqual(parseColor("123"), undefined);
|
||||
});
|
||||
|
||||
it("should treat invalid hex as 0", function () {
|
||||
assert.strictEqual(parseColor("#AX2Y3Z"), 0xa02030);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Duration parsing", function () {
|
||||
it("should accept number of seconds as number", function () {
|
||||
assert.strictEqual(parseDuration(12.3), 12.3);
|
||||
});
|
||||
|
||||
it("should accept a legacy style duration", function () {
|
||||
assert.strictEqual(parseDuration({ secs: 12.3, nanos: 400000 }), 12.3);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,278 @@
|
|||
use crate::{set_panic_hook, JavascriptPlayer, RuffleHandle, SocketProxy, RUFFLE_GLOBAL_PANIC};
|
||||
use js_sys::Promise;
|
||||
use ruffle_core::backend::navigator::OpenURLMode;
|
||||
use ruffle_core::config::{Letterbox, NetworkingAccessMode};
|
||||
use ruffle_core::{Color, PlayerRuntime, StageAlign, StageScaleMode};
|
||||
use ruffle_render::quality::StageQuality;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen(inspectable)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RuffleInstanceBuilder {
|
||||
pub(crate) allow_script_access: bool,
|
||||
pub(crate) background_color: Option<Color>,
|
||||
pub(crate) letterbox: Letterbox,
|
||||
pub(crate) upgrade_to_https: bool,
|
||||
pub(crate) compatibility_rules: bool,
|
||||
pub(crate) base_url: Option<String>,
|
||||
pub(crate) show_menu: bool,
|
||||
pub(crate) allow_fullscreen: bool,
|
||||
pub(crate) stage_align: StageAlign,
|
||||
pub(crate) force_align: bool,
|
||||
pub(crate) quality: Option<StageQuality>,
|
||||
pub(crate) scale: Option<StageScaleMode>,
|
||||
pub(crate) force_scale: bool,
|
||||
pub(crate) frame_rate: Option<f64>,
|
||||
pub(crate) wmode: Option<String>, // TODO: Enumify? `Player` is working in strings here too...
|
||||
pub(crate) log_level: tracing::Level,
|
||||
pub(crate) max_execution_duration: Duration,
|
||||
pub(crate) player_version: Option<u8>,
|
||||
pub(crate) preferred_renderer: Option<String>, // TODO: Enumify?
|
||||
pub(crate) open_url_mode: OpenURLMode,
|
||||
pub(crate) allow_networking: NetworkingAccessMode,
|
||||
pub(crate) socket_proxy: Vec<SocketProxy>,
|
||||
pub(crate) credential_allow_list: Vec<String>,
|
||||
pub(crate) player_runtime: PlayerRuntime,
|
||||
// TODO: Add font related options
|
||||
// TODO: Add volume
|
||||
}
|
||||
|
||||
impl Default for RuffleInstanceBuilder {
|
||||
fn default() -> Self {
|
||||
// Anything available in `BaseLoadOptions` should match the default we list in the docs there.
|
||||
// Some options may be variable (eg allowScriptAccess based on URL) -
|
||||
// those should be always overriding these values in JS
|
||||
|
||||
Self {
|
||||
allow_script_access: false,
|
||||
background_color: None,
|
||||
letterbox: Letterbox::Fullscreen,
|
||||
upgrade_to_https: true,
|
||||
compatibility_rules: true,
|
||||
base_url: None,
|
||||
show_menu: true,
|
||||
allow_fullscreen: false,
|
||||
stage_align: StageAlign::empty(),
|
||||
force_align: false,
|
||||
quality: None,
|
||||
scale: None,
|
||||
force_scale: false,
|
||||
frame_rate: None,
|
||||
wmode: None,
|
||||
log_level: tracing::Level::ERROR,
|
||||
max_execution_duration: Duration::from_secs_f64(15.0),
|
||||
player_version: None,
|
||||
preferred_renderer: None,
|
||||
open_url_mode: OpenURLMode::Allow,
|
||||
allow_networking: NetworkingAccessMode::All,
|
||||
socket_proxy: vec![],
|
||||
credential_allow_list: vec![],
|
||||
player_runtime: PlayerRuntime::FlashPlayer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl RuffleInstanceBuilder {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setAllowScriptAccess")]
|
||||
pub fn set_allow_script_access(&mut self, value: bool) {
|
||||
self.allow_script_access = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setBackgroundColor")]
|
||||
pub fn set_background_color(&mut self, value: Option<u32>) {
|
||||
self.background_color = value.map(|rgb| Color::from_rgb(rgb, 255));
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setUpgradeToHttps")]
|
||||
pub fn set_upgrade_to_https(&mut self, value: bool) {
|
||||
self.upgrade_to_https = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setCompatibilityRules")]
|
||||
pub fn set_compatibility_rules(&mut self, value: bool) {
|
||||
self.compatibility_rules = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setLetterbox")]
|
||||
pub fn set_letterbox(&mut self, value: &str) {
|
||||
self.letterbox = match value {
|
||||
"off" => Letterbox::Off,
|
||||
"fullscreen" => Letterbox::Fullscreen,
|
||||
"on" => Letterbox::On,
|
||||
_ => return,
|
||||
};
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setBaseUrl")]
|
||||
pub fn set_base_url(&mut self, value: Option<String>) {
|
||||
self.base_url = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setShowMenu")]
|
||||
pub fn set_show_menu(&mut self, value: bool) {
|
||||
self.show_menu = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setAllowFullscreen")]
|
||||
pub fn set_allow_fullscreen(&mut self, value: bool) {
|
||||
self.allow_fullscreen = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setStageAlign")]
|
||||
pub fn set_stage_align(&mut self, value: &str) {
|
||||
// [NA] This is weird. Do we really need this?
|
||||
|
||||
// Chars get converted into flags.
|
||||
// This means "tbbtlbltblbrllrbltlrtbl" is valid, resulting in "TBLR".
|
||||
let mut align = StageAlign::default();
|
||||
for c in value.bytes().map(|c| c.to_ascii_uppercase()) {
|
||||
match c {
|
||||
b'T' => align.insert(StageAlign::TOP),
|
||||
b'B' => align.insert(StageAlign::BOTTOM),
|
||||
b'L' => align.insert(StageAlign::LEFT),
|
||||
b'R' => align.insert(StageAlign::RIGHT),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
self.stage_align = align;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setForceAlign")]
|
||||
pub fn set_force_align(&mut self, value: bool) {
|
||||
self.force_align = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setQuality")]
|
||||
pub fn set_quality(&mut self, value: &str) {
|
||||
self.quality = match value {
|
||||
"low" => Some(StageQuality::Low),
|
||||
"medium" => Some(StageQuality::Medium),
|
||||
"high" => Some(StageQuality::High),
|
||||
"best" => Some(StageQuality::Best),
|
||||
"8x8" => Some(StageQuality::High8x8),
|
||||
"8x8linear" => Some(StageQuality::High8x8Linear),
|
||||
"16x16" => Some(StageQuality::High16x16),
|
||||
"16x16linear" => Some(StageQuality::High16x16Linear),
|
||||
_ => return,
|
||||
};
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setScale")]
|
||||
pub fn set_scale(&mut self, value: &str) {
|
||||
self.scale = match value {
|
||||
"exactfit" => Some(StageScaleMode::ExactFit),
|
||||
"noborder" => Some(StageScaleMode::NoBorder),
|
||||
"noscale" => Some(StageScaleMode::NoScale),
|
||||
"showall" => Some(StageScaleMode::ShowAll),
|
||||
_ => return,
|
||||
};
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setForceScale")]
|
||||
pub fn set_force_scale(&mut self, value: bool) {
|
||||
self.force_scale = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setFrameRate")]
|
||||
pub fn set_frame_rate(&mut self, value: Option<f64>) {
|
||||
self.frame_rate = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setWmode")]
|
||||
pub fn set_wmode(&mut self, value: Option<String>) {
|
||||
self.wmode = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setLogLevel")]
|
||||
pub fn set_log_level(&mut self, value: &str) {
|
||||
if let Ok(level) = tracing::Level::from_str(value) {
|
||||
self.log_level = level;
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setMaxExecutionDuration")]
|
||||
pub fn set_max_execution_duration(&mut self, value: f64) {
|
||||
self.max_execution_duration = Duration::from_secs_f64(value);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setPlayerVersion")]
|
||||
pub fn set_player_version(&mut self, value: Option<u8>) {
|
||||
self.player_version = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setPreferredRenderer")]
|
||||
pub fn set_preferred_renderer(&mut self, value: Option<String>) {
|
||||
self.preferred_renderer = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setOpenUrlMode")]
|
||||
pub fn set_open_url_mode(&mut self, value: &str) {
|
||||
self.open_url_mode = match value {
|
||||
"allow" => OpenURLMode::Allow,
|
||||
"confirm" => OpenURLMode::Confirm,
|
||||
"deny" => OpenURLMode::Deny,
|
||||
_ => return,
|
||||
};
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setAllowNetworking")]
|
||||
pub fn set_allow_networking(&mut self, value: &str) {
|
||||
self.allow_networking = match value {
|
||||
"all" => NetworkingAccessMode::All,
|
||||
"internal" => NetworkingAccessMode::Internal,
|
||||
"none" => NetworkingAccessMode::None,
|
||||
_ => return,
|
||||
};
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "addSocketProxy")]
|
||||
pub fn add_socket_proxy(&mut self, host: String, port: u16, proxy_url: String) {
|
||||
self.socket_proxy.push(SocketProxy {
|
||||
host,
|
||||
port,
|
||||
proxy_url,
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setCredentialAllowList")]
|
||||
pub fn set_credential_allow_list(&mut self, value: Vec<String>) {
|
||||
self.credential_allow_list = value;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setPlayerRuntime")]
|
||||
pub fn set_player_runtime(&mut self, value: &str) {
|
||||
self.player_runtime = match value {
|
||||
"air" => PlayerRuntime::AIR,
|
||||
"flashPlayer" => PlayerRuntime::FlashPlayer,
|
||||
_ => return,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: This should be split into two methods that either load url or load data
|
||||
// Right now, that's done immediately afterwards in TS
|
||||
pub async fn build(&self, parent: HtmlElement, js_player: JavascriptPlayer) -> Promise {
|
||||
let copy = self.clone();
|
||||
wasm_bindgen_futures::future_to_promise(async move {
|
||||
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.
|
||||
return Err("Ruffle is panicking!".into());
|
||||
}
|
||||
set_panic_hook();
|
||||
|
||||
let ruffle = RuffleHandle::new_internal(parent, js_player, copy)
|
||||
.await
|
||||
.map_err(|err| JsValue::from(format!("Error creating player: {}", err)))?;
|
||||
Ok(JsValue::from(ruffle))
|
||||
})
|
||||
}
|
||||
}
|
278
web/src/lib.rs
278
web/src/lib.rs
|
@ -3,6 +3,7 @@
|
|||
|
||||
//! Ruffle web frontend.
|
||||
mod audio;
|
||||
mod builder;
|
||||
mod external_interface;
|
||||
mod input;
|
||||
mod log_adapter;
|
||||
|
@ -10,20 +11,19 @@ mod navigator;
|
|||
mod storage;
|
||||
mod ui;
|
||||
|
||||
use crate::builder::RuffleInstanceBuilder;
|
||||
use external_interface::{external_to_js_value, js_to_external_value, JavascriptInterface};
|
||||
use input::{web_key_to_codepoint, web_to_ruffle_key_code, web_to_ruffle_text_control};
|
||||
use js_sys::{Error as JsError, Promise, Uint8Array};
|
||||
use ruffle_core::backend::navigator::OpenURLMode;
|
||||
use js_sys::{Error as JsError, Uint8Array};
|
||||
use ruffle_core::backend::ui::FontDefinition;
|
||||
use ruffle_core::compatibility_rules::CompatibilityRules;
|
||||
use ruffle_core::config::{Letterbox, NetworkingAccessMode};
|
||||
use ruffle_core::config::NetworkingAccessMode;
|
||||
use ruffle_core::context::UpdateContext;
|
||||
use ruffle_core::events::{MouseButton, MouseWheelDelta, TextControlCode};
|
||||
use ruffle_core::tag_utils::SwfMovie;
|
||||
use ruffle_core::{swf, DefaultFont};
|
||||
use ruffle_core::{
|
||||
Color, Player, PlayerBuilder, PlayerEvent, PlayerRuntime, SandboxType, StageAlign,
|
||||
StageScaleMode, StaticCallstack, ViewportDimensions,
|
||||
Player, PlayerBuilder, PlayerEvent, SandboxType, StaticCallstack, ViewportDimensions,
|
||||
};
|
||||
use ruffle_render::quality::StageQuality;
|
||||
use ruffle_video_software::backend::SoftwareVideoBackend;
|
||||
|
@ -34,7 +34,6 @@ use std::rc::Rc;
|
|||
use std::str::FromStr;
|
||||
use std::sync::Once;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use std::{cell::RefCell, error::Error, num::NonZeroI32};
|
||||
use tracing_subscriber::layer::{Layered, SubscriberExt};
|
||||
use tracing_subscriber::registry::Registry;
|
||||
|
@ -188,129 +187,7 @@ extern "C" {
|
|||
fn display_unsupported_video(this: &JavascriptPlayer, url: &str);
|
||||
}
|
||||
|
||||
fn deserialize_color<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let color: Option<String> = serde::Deserialize::deserialize(deserializer)?;
|
||||
let color = match color {
|
||||
Some(color) => color,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Parse classic HTML hex color (XXXXXX or #XXXXXX), attempting to match browser behavior.
|
||||
// Optional leading #.
|
||||
let color = color.strip_prefix('#').unwrap_or(&color);
|
||||
|
||||
// Fail if less than 6 digits.
|
||||
let color = match color.get(..6) {
|
||||
Some(color) => color,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let rgb = color.chars().fold(0, |acc, c| {
|
||||
// Each char represents 4-bits. Invalid hex digit is allowed (converts to 0).
|
||||
let digit = c.to_digit(16).unwrap_or_default();
|
||||
(acc << 4) | digit
|
||||
});
|
||||
Ok(Some(Color::from_rgb(rgb, 255)))
|
||||
}
|
||||
|
||||
fn deserialize_log_level<'de, D>(deserializer: D) -> Result<tracing::Level, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
let value: String = serde::Deserialize::deserialize(deserializer)?;
|
||||
tracing::Level::from_str(&value).map_err(Error::custom)
|
||||
}
|
||||
|
||||
fn deserialize_player_runtime<'de, D>(deserializer: D) -> Result<PlayerRuntime, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value: String = serde::Deserialize::deserialize(deserializer)?;
|
||||
Ok(match value.as_str() {
|
||||
"air" => PlayerRuntime::AIR,
|
||||
"flashPlayer" => PlayerRuntime::FlashPlayer,
|
||||
_ => Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
struct DurationVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for DurationVisitor {
|
||||
type Value = Duration;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str(
|
||||
"Either a non-negative number (indicating seconds) or a {secs: number, nanos: number}."
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(Duration::from_secs_f64(if value < 1 {
|
||||
1.0
|
||||
} else {
|
||||
value as f64
|
||||
}))
|
||||
}
|
||||
|
||||
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(Duration::from_secs_f64(if value.is_nan() || value < 1.0 {
|
||||
1.0
|
||||
} else {
|
||||
value
|
||||
}))
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut secs = None;
|
||||
let mut nanos = None;
|
||||
while let Some(key) = map.next_key::<String>()? {
|
||||
let key_s = key.as_str();
|
||||
|
||||
match key_s {
|
||||
"secs" => {
|
||||
if secs.is_some() {
|
||||
return Err(Error::duplicate_field("secs"));
|
||||
}
|
||||
secs = Some(map.next_value()?);
|
||||
}
|
||||
"nanos" => {
|
||||
if nanos.is_some() {
|
||||
return Err(Error::duplicate_field("nanos"));
|
||||
}
|
||||
nanos = Some(map.next_value()?);
|
||||
}
|
||||
_ => return Err(Error::unknown_field(key_s, &["secs", "nanos"])),
|
||||
}
|
||||
}
|
||||
let secs = secs.ok_or_else(|| Error::missing_field("secs"))?;
|
||||
let nanos = nanos.ok_or_else(|| Error::missing_field("nanos"))?;
|
||||
Ok(Duration::new(secs, nanos))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(DurationVisitor)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SocketProxy {
|
||||
host: String,
|
||||
|
@ -319,64 +196,6 @@ pub struct SocketProxy {
|
|||
proxy_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Config {
|
||||
allow_script_access: bool,
|
||||
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
background_color: Option<Color>,
|
||||
|
||||
letterbox: Letterbox,
|
||||
|
||||
upgrade_to_https: bool,
|
||||
|
||||
compatibility_rules: bool,
|
||||
|
||||
#[serde(rename = "base")]
|
||||
base_url: Option<String>,
|
||||
|
||||
#[serde(rename = "menu")]
|
||||
show_menu: bool,
|
||||
|
||||
allow_fullscreen: bool,
|
||||
|
||||
salign: Option<String>,
|
||||
|
||||
force_align: bool,
|
||||
|
||||
quality: Option<String>,
|
||||
|
||||
scale: Option<String>,
|
||||
|
||||
force_scale: bool,
|
||||
|
||||
frame_rate: Option<f64>,
|
||||
|
||||
wmode: Option<String>,
|
||||
|
||||
#[serde(deserialize_with = "deserialize_log_level")]
|
||||
log_level: tracing::Level,
|
||||
|
||||
#[serde(deserialize_with = "deserialize_duration")]
|
||||
max_execution_duration: Duration,
|
||||
|
||||
player_version: Option<u8>,
|
||||
|
||||
preferred_renderer: Option<String>,
|
||||
|
||||
open_url_mode: OpenURLMode,
|
||||
|
||||
allow_networking: NetworkingAccessMode,
|
||||
|
||||
socket_proxy: Vec<SocketProxy>,
|
||||
|
||||
credential_allow_list: Vec<String>,
|
||||
|
||||
#[serde(deserialize_with = "deserialize_player_runtime")]
|
||||
player_runtime: PlayerRuntime,
|
||||
}
|
||||
|
||||
/// Metadata about the playing SWF file to be passed back to JavaScript.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -394,27 +213,6 @@ struct MovieMetadata {
|
|||
|
||||
#[wasm_bindgen]
|
||||
impl RuffleHandle {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(parent: HtmlElement, js_player: JavascriptPlayer, config: JsValue) -> Promise {
|
||||
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.
|
||||
return Err("Ruffle is panicking!".into());
|
||||
}
|
||||
set_panic_hook();
|
||||
|
||||
let ruffle = Self::new_internal(parent, js_player, config)
|
||||
.await
|
||||
.map_err(|err| JsValue::from(format!("Error creating player: {}", err)))?;
|
||||
Ok(JsValue::from(ruffle))
|
||||
})
|
||||
}
|
||||
|
||||
/// Stream an arbitrary movie file from (presumably) the Internet.
|
||||
///
|
||||
/// This method should only be called once per player.
|
||||
|
@ -646,7 +444,7 @@ impl RuffleHandle {
|
|||
async fn new_internal(
|
||||
parent: HtmlElement,
|
||||
js_player: JavascriptPlayer,
|
||||
config: Config,
|
||||
config: RuffleInstanceBuilder,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
// Redirect Log to Tracing if it isn't already
|
||||
let _ = tracing_log::LogTracer::builder()
|
||||
|
@ -730,63 +528,9 @@ impl RuffleHandle {
|
|||
} else {
|
||||
CompatibilityRules::empty()
|
||||
})
|
||||
.with_quality(
|
||||
config
|
||||
.quality
|
||||
.and_then(|q| {
|
||||
let quality = match q.to_ascii_lowercase().as_str() {
|
||||
"low" => StageQuality::Low,
|
||||
"medium" => StageQuality::Medium,
|
||||
"high" => StageQuality::High,
|
||||
"best" => StageQuality::Best,
|
||||
"8x8" => StageQuality::High8x8,
|
||||
"8x8linear" => StageQuality::High8x8Linear,
|
||||
"16x16" => StageQuality::High16x16,
|
||||
"16x16linear" => StageQuality::High16x16Linear,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(quality)
|
||||
})
|
||||
.unwrap_or(default_quality),
|
||||
)
|
||||
.with_align(
|
||||
config
|
||||
.salign
|
||||
.map(|s| {
|
||||
// Chars get converted into flags.
|
||||
// This means "tbbtlbltblbrllrbltlrtbl" is valid, resulting in "TBLR".
|
||||
let mut align = StageAlign::default();
|
||||
for c in s.bytes().map(|c| c.to_ascii_uppercase()) {
|
||||
match c {
|
||||
b'T' => align.insert(StageAlign::TOP),
|
||||
b'B' => align.insert(StageAlign::BOTTOM),
|
||||
b'L' => align.insert(StageAlign::LEFT),
|
||||
b'R' => align.insert(StageAlign::RIGHT),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
align
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
config.force_align,
|
||||
)
|
||||
.with_scale_mode(
|
||||
config
|
||||
.scale
|
||||
.and_then(|s| {
|
||||
let scale_mode = match s.to_ascii_lowercase().as_str() {
|
||||
"exactfit" => StageScaleMode::ExactFit,
|
||||
"noborder" => StageScaleMode::NoBorder,
|
||||
"noscale" => StageScaleMode::NoScale,
|
||||
"showall" => StageScaleMode::ShowAll,
|
||||
_ => return None,
|
||||
};
|
||||
Some(scale_mode)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
config.force_scale,
|
||||
)
|
||||
.with_quality(config.quality.unwrap_or(default_quality))
|
||||
.with_align(config.stage_align, config.force_align)
|
||||
.with_scale_mode(config.scale.unwrap_or_default(), config.force_scale)
|
||||
.with_frame_rate(config.frame_rate)
|
||||
// FIXME - should this be configurable?
|
||||
.with_sandbox_type(SandboxType::Remote)
|
||||
|
@ -1421,7 +1165,7 @@ pub enum RuffleInstanceError {
|
|||
async fn create_renderer(
|
||||
builder: PlayerBuilder,
|
||||
document: &web_sys::Document,
|
||||
config: &Config,
|
||||
config: &RuffleInstanceBuilder,
|
||||
) -> Result<(PlayerBuilder, HtmlCanvasElement), Box<dyn Error>> {
|
||||
#[cfg(not(any(
|
||||
feature = "canvas",
|
||||
|
|
Loading…
Reference in New Issue