Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
115 changes: 113 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
Copy Markdown
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,119 @@
// ... 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) => {

pyodide.registerJsModule("otio_js_data", {file_name: file_name, data: evt.target.result});

console.log('file_name:', file_name);
// Get adapter
let timeline_json = pyodide.runPython(`
Comment thread
JeanChristopheMorinPerso marked this conversation as resolved.
Outdated
import tempfile

from otio_js_data import file_name, data
import opentimelineio

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

with tempfile.NamedTemporaryFile(mode=mode, suffix="." + file_name.split(".")[-1]) as f:
if not isinstance(data, str):
# Probably an ArrayBuffer. So convert to a memoryview
# https://pyodide.org/en/stable/usage/type-conversions.html#buffers
f.write(data.to_py())
else:
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
`);

// 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
Copy Markdown
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)

Copy link
Copy Markdown

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
Copy Markdown
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?

Copy link
Copy Markdown

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
Copy Markdown
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
Copy Markdown
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() {
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',
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We should move this out of there I think.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The tricky thing is that we can't upload wasm32 wheels to PyPI. So we'll have to find a place to host it.

'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;
}

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

if (window.pyodide) {
console.log("Pyodide already loaded");
removeRunDependency("pyodide");
return;
}

console.log("Waiting for pyodide");
await new Promise((resolve, reject) => {
let check = setInterval(() => {
if (window.pyodide) {
clearInterval(check);
resolve();
}
}, 50);
});
console.log("Pyodide loaded");
removeRunDependency("pyodide");
}

/**
* Once raven loads, load any OTIO timeline that was passed in.
*/
Expand All @@ -64,8 +175,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