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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ elseif(EMSCRIPTEN)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DIMGUI_DISABLE_FILE_FUNCTIONS")
set_target_properties(raven PROPERTIES SUFFIX .html)
target_link_libraries(raven PUBLIC ${LIBS})
# Make sure the shell file is rebuilt when the HTML file changes.
set_property(
TARGET raven
PROPERTY LINK_DEPENDS
${CMAKE_CURRENT_LIST_DIR}/shell_minimal.html
)
else()
target_sources(raven PUBLIC main_glfw.cpp)
endif()
Expand Down
11 changes: 11 additions & 0 deletions app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

#ifndef EMSCRIPTEN
#include "nfd.h"
#else
#include <emscripten.h>
#endif

#include "mz.h"
Expand Down Expand Up @@ -759,6 +761,13 @@ void SaveTheme() {

std::string OpenFileDialog() {
#ifdef EMSCRIPTEN
// https://stackoverflow.com/a/69935189
EM_ASM(
var file_selector = document.createElement('input');
file_selector.setAttribute('type', 'file');
file_selector.setAttribute('onchange','Module.LoadStringFromEvent(event)');
file_selector.click();
);
return "";
#else
nfdchar_t* outPath = NULL;
Expand Down Expand Up @@ -803,11 +812,13 @@ void DrawMenu() {
if (path != "")
LoadFile(path);
}
#ifndef EMSCRIPTEN
if (ImGui::MenuItem("Save As...")) {
auto path = SaveFileDialog();
if (path != "")
SaveFile(path);
}
#endif
if (ImGui::MenuItem("Revert")) {
LoadFile(appState.file_path);
}
Expand Down
106 changes: 104 additions & 2 deletions shell_minimal.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"/>
<title>Raven</title>
<script src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm getting pyodide from a CDN, but we should probably look at other options.

<style>
body { margin: 0; background-color: black }
.emscripten {
Expand Down Expand Up @@ -47,9 +48,110 @@
// ... And free it when we're done.
_free(stringOnWasmHeap);
}

// https://stackoverflow.com/a/69935189
Module.LoadStringFromEvent = function (element_event) {
const file_reader = new FileReader();
let file_name = element_event.target.files[0].name;

file_reader.onload = (evt) => {
if (file_name.endsWith(".otio")) {
timeline_json = evt.target.result;
} else {
console.log("Loading using pyodide")
// Get adapter
timeline_json = pyodide.runPython(`
import tempfile

import opentimelineio

mode = 'w'
if not isinstance(data, str):
mode = 'wb'

with tempfile.NamedTemporaryFile(mode=mode, suffix="." + file_name.split(".")[-1]) as f:
f.write(data)

f.flush()
f.seek(0)

timeline = opentimelineio.adapters.read_from_file(f.name)
output = opentimelineio.adapters.write_to_string(timeline, adapter_name='otio_json', indent=0)
output
`, {locals: pyodide.toPy({data: evt.target.result, file_name})});
}

// This is annoying, but we need to allocate memory manually on the wasm heap
// to avoid a stack overflow if you pass in a very large string.
var lengthBytes = lengthBytesUTF8(timeline_json) + 1;
var stringOnWasmHeap = _malloc(lengthBytes);
stringToUTF8(timeline_json, stringOnWasmHeap, lengthBytes);

Module.ccall("js_LoadString", "number", ["number"], [stringOnWasmHeap]);
Copy link
Member Author

Choose a reason for hiding this comment

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

We should not call this for otioz (which raven supports natively)

Choose a reason for hiding this comment

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

Does it support OTIOZ when running in WASM? If so, how well does it handle the memory mapping situation? We may end up wanting to go down the route of the new File System API (Chromium support ✅, but no Safari/Firefox I'm pretty sure), so that it doesn't have to read the whole thing into memory directly.

Copy link
Member Author

Choose a reason for hiding this comment

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

I have not tried, but I don't see why it wouldn't work.

I took a quick look at the new shiny file system API stuff but didn't go deep enough to understand the pros and cons memory wise. Do you have a link that would talk more exactly that?

Choose a reason for hiding this comment

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

https://developer.chrome.com/docs/capabilities/web-apis/file-system-access

Give this a quick glance. I don't really know this translates into WASM but my thinking was really just that we could just access the file in-situ without passing it all the way into the WASM-mapped 2GB/4GB memory this way. I just figure that with an OTIOZ when there's actually media, you'll hit the limit pretty much immediately.


// ... And free it when we're done.
_free(stringOnWasmHeap);
}
if (element_event.target.files[0].name.endsWith('.aaf')) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe otioz too? What other formats are in binary?

Copy link
Member

Choose a reason for hiding this comment

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

AAF and OTIOZ are binary, the rest are all text.

file_reader.readAsArrayBuffer(element_event.target.files[0]);
} else {
file_reader.readAsBinaryString(element_event.target.files[0]);
}
}
Module.LoadUrl = Module.cwrap("js_LoadUrl", "number", ["string"]);
}

async function initializePyodide() {
window.initializePyodidePromise = new Promise(async function(resolve, reject) {
try {
console.log("Loading pyodide");
let pyodide = await loadPyodide();
console.log("Loaded pyodide");

console.log("Installing micropip");
await pyodide.loadPackage("micropip");
console.log("Installed micropip");

console.log("Installing OTIO python bindings and plugins");
const micropip = pyodide.pyimport("micropip");
await micropip.install(
[
'https://jcmorin.dev/otio-wasm/opentimelineio-0.18.0.dev1-cp312-cp312-pyodide_2024_0_wasm32.whl',
'otio-aaf-adapter',
'otio-burnins-adapter',
'otio-xges-adapter',
'otio-ale-adapter',
'otio-hls-playlist-adapter',
'otio-fcpx-xml-adapter',
'otio-maya-sequencer-adapter',
'otio-cmx3600-adapter',
'otio-fcp-adapter',
]
);
console.log("Installed OTIO python bindings and plugins");

// This is a hack. See https://github.com/pyodide/pyodide/pull/4836
pyodide._module.reportUndefinedSymbols();

window.pyodide = pyodide;
} catch (e) {
console.error(e);
reject(e);
}
resolve();
});
}

async function waitForPyodide() {
addRunDependency("pyodide");

console.log("Waiting for pyodide");
await window.initializePyodidePromise;
console.log("Pyodide loaded");

removeRunDependency("pyodide");
}

/**
* Once raven loads, load any OTIO timeline that was passed in.
*/
Expand All @@ -64,8 +166,8 @@
var Module = {
// otioLoadUrl: "",
// otioLoadString: "",
preRun: [exposeRavenFunctions],
postRun: [ravenPostRun],
preRun: [exposeRavenFunctions, initializePyodide],
postRun: [ravenPostRun, waitForPyodide],
print: (function() {
return function(text) {
text = Array.prototype.slice.call(arguments).join(' ');
Expand Down
Loading