| <!doctype html> | |
| <html lang="en-us" data-theme="light"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> | |
| <title>Run MP4Box & GPAC in your browser !</title> | |
| <link rel="icon" type="image/x-icon" href="favicon-32x32.png"> | |
| <link | |
| rel="stylesheet" | |
| href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.red.min.css" | |
| /> | |
| <style> | |
| :root { | |
| font-size: 14px; | |
| } | |
| body { | |
| font-family: arial; | |
| margin: 0; | |
| padding: none; | |
| } | |
| #logo { | |
| display: flex; | |
| justify-content: flex-start; | |
| align-items: center; | |
| } | |
| #menu { | |
| height: 5em; | |
| color: rgb(20, 20, 20); | |
| } | |
| nav#menu li { | |
| padding-top: 0; | |
| padding-bottom: 0; | |
| } | |
| #menu label { | |
| margin: 0; | |
| } | |
| #logo svg:first-child { | |
| margin-right: .5em; | |
| } | |
| svg.icon-theme-toggle.moon g circle { | |
| transform: scale(1.4); | |
| transition-delay: 0s; | |
| } | |
| svg.icon-theme-toggle.moon g path { | |
| transform: scale(.75); | |
| transition-delay: 0s; | |
| } | |
| svg.icon-theme-toggle g circle, svg.icon-theme-toggle g path { | |
| transform-origin: center; | |
| transition: transform calc(var(--theme-toggle-duration)* .65) cubic-bezier(0,0,0,1.25) calc(var(--theme-toggle-duration)* .35); | |
| } | |
| svg.icon-theme-toggle :first-child path { | |
| transition-duration: calc(var(--theme-toggle-duration)* .6); | |
| transition-property: transform, d; | |
| transition-timing-function: cubic-bezier(0,0,.5,1); | |
| } | |
| svg.icon-theme-toggle.moon :first-child path { | |
| d: path("M-9 3h25a1 1 0 0017 13v30H0Z"); | |
| transition-delay: calc(var(--theme-toggle-duration)* .4); | |
| transition-timing-function: cubic-bezier(0,0,0,1.25); | |
| } | |
| .console-wrapper { | |
| background-color: rgb(8, 8, 8); | |
| } | |
| .console { | |
| width: 100%; | |
| background-color: black; | |
| color: white; | |
| font-family: 'Lucida Console', Monaco, monospace; | |
| overflow-y: scroll; | |
| padding: 0.5rem !important; | |
| } | |
| .cwd { | |
| color: var(--pico-primary-hover); | |
| height: 8vh; | |
| } | |
| #cmd_div { | |
| font-size: small; | |
| } | |
| #cmd { | |
| height: 9vh; | |
| margin: 0; | |
| } | |
| #run, #cancel { | |
| width: 6em; | |
| } | |
| #fsRunForm{ | |
| display: flex; | |
| gap: 10px; | |
| } | |
| #flush{ | |
| width: 8em; | |
| } | |
| #output-wrapper { | |
| display: flex; | |
| align-items: top; | |
| gap: 0; | |
| margin: none; | |
| padding: none; | |
| } | |
| #output { | |
| height: 65vh; | |
| resize: vertical; | |
| } | |
| #output:not([readonly]):is(:active,:focus) { | |
| border: var(--pico-border-width) solid var(--pico-border-color); | |
| } | |
| #controls { | |
| height: 10vh; | |
| gap: 1em; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| /* @FIXME overlay fullscren on canvas */ | |
| #fullscreen { | |
| display: none; | |
| } | |
| #status { | |
| font-size: small; | |
| font-weight: bold; | |
| align-items: center; | |
| margin-bottom: 0.5em; | |
| color: var(--pico-primary); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav id="menu" class="container-fluid"> | |
| <ul> | |
| <li> | |
| <a id="logo" href="https://gpac.io/" target="_blank"> | |
| <svg viewBox="0 0 2000 2000" width="2.5em" height="2.5em" xml:space="preserve"> | |
| <defs> | |
| <style> | |
| .cls-1{fill:#8f182b;} | |
| .cls-2{fill:#ef524f;} | |
| .cls-3{fill:#ba2030;} | |
| .cls-4{fill:#cd2044;} | |
| .cls-5{fill:#d92f32;} | |
| .cls-6{fill:#1a1c4a;} | |
| </style> | |
| </defs> | |
| <g transform-origin="997.7 537.6"> | |
| <polygon class="cls-1" points="1291.1 306.35 704.27 306.35 997.68 768.75 1291.1 306.35 1291.1 306.35"/> | |
| <rect class="cls-6" x="542.76" y="761.44" width="906.68" height="906.68" rx="453.34"/> | |
| <animateTransform id="logo_anim" attributeName="transform" attributeType="XML" type="scale" values="1 1; -1 1; 1 1" keyTimes="0; 0.5; 1" dur="1s" repeatCount="indefinite"/> | |
| </g> | |
| <g> | |
| <path class="cls-2" d="M1000,330.1a666.82,666.82,0,1,1-260.67,52.52A665.55,665.55,0,0,1,1000,330.1m0-284.51C472.89,45.59,45.59,472.89,45.59,1000s427.3,954.41,954.41,954.41,954.41-427.3,954.41-954.41S1527.11,45.59,1000,45.59Z"/> | |
| <path class="cls-3" d="M1408.86,1857.84a950.83,950.83,0,0,1-403.56,89.25c-527.12,0-954.42-427.3-954.42-954.41,0-188.39,54.58-364,148.81-512-1.85,5.14-3.66,10.31-5.39,15.49C121.83,627.22,81,775.58,81,932.75c0,531.12,466.61,961.67,1042.19,961.67A1124.55,1124.55,0,0,0,1408.86,1857.84Z"/> | |
| <path class="cls-4" d="M1683.45,1664.24a957,957,0,0,1-274.59,193.6,1124.55,1124.55,0,0,1-285.7,36.58C547.58,1894.42,81,1463.87,81,932.75c0-157.17,40.86-305.53,113.33-436.54-.49,1.45-1,2.9-1.46,4.34,0,0,0,0,0,0s0,0,0,.05A881.86,881.86,0,0,0,118,856.32c0,523.63,460,948.1,1027.47,948.1C1342.73,1804.42,1527,1753.14,1683.45,1664.24Z"/> | |
| <path class="cls-5" d="M1834.94,1464.76a956.45,956.45,0,0,1-151.47,199.46s0,0,0,0c-156.46,88.9-340.72,140.18-537.94,140.18C578.06,1804.42,118,1380,118,856.32A881.86,881.86,0,0,1,192.8,500.64a865.83,865.83,0,0,0-44,272.85c0,514.46,452,931.5,1009.5,931.5C1418.62,1705,1655.92,1614.09,1834.94,1464.76Z"/> | |
| </g> | |
| <g transform="translate(5.6 15)"> | |
| <text id="progress" stroke="black" stroke-width="0.1" fill="white" font-size="3.5" font-weight="bold"> </text> | |
| </g> | |
| </svg> | |
| <svg id="gpac-brand" data-name="gpac-brand" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 671.14 188.48" height="1em" > | |
| <defs> | |
| <style>.cls-1{fill:#1a1c4a;}</style> | |
| </defs> | |
| <path class="cls-1" d="M509.26,203h-79.6L415,243.58H389.86l66-187.82h27.49l65.72,187.82H523.93ZM469.46,85.08l-33,98.54h66Z" transform="translate(-64.43 -55.76)"/> | |
| <path class="cls-1" d="M651,56.9c39.54,0,71,19.9,84.58,55.25H707c-9.95-22-29.33-34.3-56-34.3-38.22,0-66.77,27.75-66.77,72.53,0,44.51,28.55,72.27,66.77,72.27,26.71,0,46.09-12.31,56-34h28.54c-13.62,35.08-45,54.72-84.58,54.72-51,0-91.12-38.23-91.12-93S599.94,56.9,651,56.9Z" transform="translate(-64.43 -55.76)"/> | |
| <path class="cls-1" d="M240.13,113.06H211.59c-10-21.21-29.85-33.52-55.51-33.52-38.49,0-67.3,27.76-67.3,71.75,0,43.73,28.81,71.48,67.3,71.48,35.87,0,60.48-23,64.15-59.18h-72.8V144.22h98.2v18.33c-4.19,45.3-40.07,81.69-89.55,81.69-51.32,0-91.65-38.23-91.65-93s40.33-93.22,91.65-93.22C194.57,58.07,226.51,78,240.13,113.06Z" transform="translate(-64.43 -55.76)"/> | |
| <path class="cls-1" d="M336,162.84H300.88v80.74H277.05v-187H336c42.94,0,63.1,23.57,63.1,53.42C399.07,137.7,381.26,162.84,336,162.84Zm0-19.64c27.23,0,38.75-12.83,38.75-33.25,0-21.21-11.52-33.78-38.75-33.78H300.88v67Z" transform="translate(-64.43 -55.76)"/> | |
| <rect class="cls-1" x="145.07" y="88.42" width="97.98" height="18.62"/> | |
| </svg> | |
| </a> | |
| </li> | |
| <li> | |
| <em>experimental wasm playground</em> | |
| </li> | |
| <li> | |
| <label> | |
| <input id="threading" name="threading" type="checkbox" role="switch" /> | |
| <em>threaded</em> | |
| </label> | |
| </li> | |
| </ul> | |
| <ul> | |
| <li> | |
| <a class="contrast" aria-label="Switch to dark mode" onclick=" | |
| const html = document.getElementsByTagName('html')[0]; | |
| const theme = html.getAttribute('data-theme'); | |
| const target = theme == 'light' ? 'dark' : 'light'; | |
| html.setAttribute('data-theme', target); | |
| this.setAttribute('aria-label', `Switch to ${theme} mode`); | |
| const toggle = document.querySelector('svg.icon-theme-toggle'); | |
| if(target == 'light'){ | |
| toggle.classList.remove('moon'); | |
| } else { | |
| toggle.classList.add('moon'); | |
| } | |
| "> | |
| <svg id="icon-theme-toggle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 32 32" fill="currentColor" class="icon-theme-toggle"> | |
| <clipPath id="theme-toggle-cutout"><path d="M0-11h25a1 1 0 0017 13v30H0Z"></path></clipPath> | |
| <g clip-path="url(#theme-toggle-cutout)"> | |
| <circle cx="16" cy="16" r="8.4"></circle> | |
| <path d="M18.3 3.2c0 1.3-1 2.3-2.3 2.3s-2.3-1-2.3-2.3S14.7.9 16 .9s2.3 1 2.3 2.3zm-4.6 25.6c0-1.3 1-2.3 2.3-2.3s2.3 1 2.3 2.3-1 2.3-2.3 2.3-2.3-1-2.3-2.3zm15.1-10.5c-1.3 0-2.3-1-2.3-2.3s1-2.3 2.3-2.3 2.3 1 2.3 2.3-1 2.3-2.3 2.3zM3.2 13.7c1.3 0 2.3 1 2.3 2.3s-1 2.3-2.3 2.3S.9 17.3.9 16s1-2.3 2.3-2.3zm5.8-7C9 7.9 7.9 9 6.7 9S4.4 8 4.4 6.7s1-2.3 2.3-2.3S9 5.4 9 6.7zm16.3 21c-1.3 0-2.3-1-2.3-2.3s1-2.3 2.3-2.3 2.3 1 2.3 2.3-1 2.3-2.3 2.3zm2.4-21c0 1.3-1 2.3-2.3 2.3S23 7.9 23 6.7s1-2.3 2.3-2.3 2.4 1 2.4 2.3zM6.7 23C8 23 9 24 9 25.3s-1 2.3-2.3 2.3-2.3-1-2.3-2.3 1-2.3 2.3-2.3z"></path> | |
| </g> | |
| </svg> | |
| </a> | |
| </li> | |
| <li> | |
| <a href="#help" target="blank">Console usage</a> | |
| </li> | |
| <li> | |
| <a href="https://wiki.gpac.io/" target="blank">GPAC's wiki</a> | |
| </li> | |
| <li> | |
| <a href="https://github.com/gpac/gpac/labels/wasm" target="blank">Support</a> | |
| </li> | |
| <li> | |
| <a href="https://github.com/gpac/gpac" target="blank">Github</a> | |
| </li> | |
| </ul> | |
| </nav> | |
| <div id="status" class="container-fluid"> | |
| Downloading... | |
| </div> | |
| <div id="output-wrapper" class="container-fluid"> | |
| <div class="console-wrapper"> | |
| <canvas id="canvas" tabindex=-1 width="0" height="0"></canvas> | |
| </div> | |
| <div id="output" class="console"></div> | |
| </div> | |
| <!-- command line input --> | |
| <div id="cmd_div" class="container-fluid"> | |
| <fieldset class="console-wrapper"> | |
| <textarea id="cmd" class="console" placeholder="Enter a command ..." spellcheck="false" autofocus autocomplete="false" | |
| onkeypress=" | |
| if (event.keyCode == 13) { | |
| if (!is_custfs) { | |
| document.getElementById('output').innerHTML += this.value + '<br>'; | |
| } | |
| process_command(); | |
| event.preventDefault(); | |
| } | |
| " onkeyup=" | |
| let idx=cmd_idx; | |
| if (event.keyCode == 38) { idx++; } // UP | |
| else if (event.keyCode == 40) { idx--;} // DOWN | |
| else { | |
| if (event.keyCode == 17) ctrl_down=false; | |
| else if (event.keyCode == 18) opt_down=false; | |
| if (document.activeElement === LIBGPAC['canvas']) { | |
| event.preventDefault(); | |
| set_status('canvas is def'); | |
| } | |
| return; | |
| } | |
| if (idx>=cmd_history.length) return; | |
| var cmd_e = document.getElementById('cmd'); | |
| if (idx<0) { | |
| cmd_idx = -1; | |
| cmd_e.value = ''; | |
| return; | |
| } | |
| cmd_idx = idx; | |
| cmd_e.value = cmd_history[cmd_history.length-1-idx]; | |
| if (event.ctrlKey) { | |
| switch (event.keyCode) { | |
| case 67: // Ctrl+C | |
| setCommand(''); | |
| break; | |
| case 76: // Ctrl+L | |
| outputElement.innerHTML = ''; | |
| break; | |
| case 65: // Ctrl+A | |
| cmdElement.selectionStart = 0; | |
| cmdElement.selectionEnd = 0; | |
| break; | |
| case 69: // Ctrl+E | |
| cmdElement.selectionStart = cmdElement.value.length; | |
| cmdElement.selectionEnd = cmdElement.value.length; | |
| break; | |
| } | |
| } | |
| " onkeydown=" | |
| if (event.keyCode == 17) ctrl_down=true; | |
| else if (event.keyCode == 18) opt_down=true; | |
| if (document.activeElement === LIBGPAC['canvas']) | |
| { | |
| event.preventDefault(); | |
| } | |
| "></textarea> | |
| </fieldset> | |
| </div> | |
| <!-- session controls --> | |
| <div id="controls" class="container-fluid"> | |
| <select disabled id="commands" aria-label="select an example"> | |
| <option selected="" value="-1">Select an example</option> | |
| </select> | |
| <input id="run" type="button" value="Run ⤶" disabled/> | |
| <input id="cancel" type="button" value="Cancel" hidden/> | |
| <form id="fsRunForm" hidden> | |
| <label> | |
| <input id="reports" type="checkbox" role="switch" onclick="GPAC.toggle_reporting(4);"/> | |
| reporting | |
| </label> | |
| <fieldset role="group"> | |
| <select name="flush" aria-label="Select Flush behavior on abort" id="flush"> | |
| <option value="1">Full Flush</option> | |
| <option value="2">Fast Flush</option> | |
| <option value="3" selected>No Flush</option> | |
| </select> | |
| <input id="abort" type="button" name="abort" value="Abort" onclick=" | |
| let el = document.getElementById('flush'); | |
| let val = el.options[el.selectedIndex].value; | |
| GPAC.toggle_reporting(val); | |
| console.log(val); | |
| " /> | |
| </fieldset> | |
| </form> | |
| <input type="button" value="Fullscreen" id="fullscreen" onclick=" | |
| LIBGPAC['canvas'].requestFullscreen(); | |
| "> | |
| </div> | |
| <div id="help" class="container-fluid"> | |
| <h2>Console usage</h2> | |
| <p>This page allows running the <strong>MP4Box</strong> and <strong>GPAC</strong> applications in web browsers.</p> | |
| <p>The webassembly build is experimental. It tries to uses webcodecs whenever decoding or encoding is involved. If webcodecs can't be used, it falls back to software implementation running in wasm.</p> | |
| <p>Select an example and make edits, or type in your own command. Finally press <code>Enter</code> key or click the <code>Run</code> button.</p> | |
| <p>Remote <strong>DASH</strong> and <strong>HLS</strong> playlists can be used as input, provided that they are hosted with proper <strong>CORS</strong> configurations.</p> | |
| <p>The console also provides basic shell file system, commands to import media export media files, and support advanced gpac configuration, as described below:</p> | |
| <h3> | |
| File import / export | |
| </h3> | |
| <ul> | |
| <li><code>imp [-d] [N]</code>: imports files or directory <code>-d</code> to local filesystem file.</li> | |
| <li><code>imp</code> : imports single file to current dir using its native name</li> | |
| <li><code>imp NAME</code> : imports single file as <code>NAME</code></li> | |
| <li><code>imp NAME/</code> : imports one or more files in <code>NAME</code> using their native name</li> | |
| <li><code>imp -d</code> : imports a directory to current dir</li> | |
| <li><code>imp -d NAME</code> : imports a directory as <code>NAME</code></li> | |
| <li><code>exp NAME</code>: exports given file <code>NAME</code></li> | |
| </ul> | |
| <p><a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a> is used for persistent storage, mounted at <code>/idbfs</code>. Typing any command will synchronize the file system.</p> | |
| <h4>Handling large files</h4> | |
| <p>The variable <code>$(NAME)</code> can be used to design a file not on Emscripten's file system. | |
| The file will be handled as a stream (not copied). This is the recommended methods for handling large files.</p> | |
| <p>If <code>NAME</code> is an absolute URL other than <code>file://</code> scheme: | |
| </p><ul> | |
| <li> | |
| if used for an input, it will be pulled progressively using <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">Fetch API</a>. | |
| </li> | |
| <li> | |
| if used for an output, it will be pushed progressively using <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">Fetch API</a>. If writable streams fail, the file will be stored in memory until final PUT or canceled.<p></p> | |
| </li> | |
| </ul> | |
| <p>Otherwise, <code>NAME</code> designs a file to be picked on the user's file system | |
| </p><ul> | |
| <li> | |
| if used for an input, a file picker will be shown and status text will indicate the corresponding <code>NAME</code> | |
| </li> | |
| <li> | |
| if used for an output, a file saver will be shown with the given <code>NAME</code>. If writable stream failure, the file will be stored in memory until final write or canceled.<p></p> | |
| </li> | |
| </ul> | |
| <p>For output files, the extension will be used to derive the output format, e.g. <code>($file.ts)</code> will use MPEG-2 TS. | |
| Use <code>$(file):ext=ts</code> to override format if desired. <em>This syntax CANNOT work when editing files (MP4Box, dasher context, etc...).</em></p> | |
| <h3>Shell commands</h3> | |
| <ul> | |
| <li><code>↑</code><code>↓</code> : Use up & down arrows can be used for command history.</li> | |
| <li><code>ls [N]</code>: list files in <code>N</code> or in current directory if <code>N</code> not set or print file N info - use 'ls -l N' for details.</li> | |
| <li><code>cd [NAME]</code>: changes current working directory, use Emscripten home if <code>NAME</code> not provided</li> | |
| <li><code>cwd</code>: prints current working directory</li> | |
| <li><code>clear</code>: clears console output</li> | |
| <li><code>mkdir NAME</code>: creates directory <code>NAME</code></li> | |
| <li><code>rm NAME</code>: removes file or empty directory <code>NAME</code></li> | |
| <li><code>mv N1 N2</code>: moves file <code>N1</code> to <code>N2</code></li> | |
| <li><code>cp N1 N2</code>: copies file <code>N1</code> to <code>N2</code> (no recursive copy support)</li> | |
| <li><code>show NAME</code>: shows content of file <code>NAME</code></li> | |
| <li><code>edit NAME</code>: edits content of file <code>NAME</code>, use <code>save</code> to save and close, <code>close</code> to discard and close</li> | |
| </ul> | |
| <h3>Configuration commands</h3> | |
| <ul> | |
| <li class="threaded"><code>worker</code>: toggles worker mode on or off (off)</li> | |
| <li class="threaded"><code>threads</code>: views number of pre-allocated workers</li> | |
| <li><code>stack</code>: prints available size in stack</li> | |
| <li><code>clearh [N]</code>: clears command history. If <code>N</code> is set, only keeps last <code>N</code> items, otherwise clears everything</li> | |
| <li><code>fetch</code>: toggles fetch support in libgpac</li> | |
| <li><code>bsize</code>: sets block size used to read from local disk (default 10000)</li> | |
| <li><code>eval</code>: evaluates JS code</li> | |
| </ul> | |
| <h4>Performances</h4> | |
| <p>The default behaviour for gpac app is to run on the browser/worker main loop in non-blocking mode, running the session at most 100 times before returning control.</p> | |
| <p>This can be configured when calling gpac (see <code>gpac -hx</code>), for example: | |
| </p><ul> | |
| <li> | |
| <code>-step=-1</code> will block the main browser/worker thread (WILL DEADLOCK if some filters use JS promises or other async tools) | |
| </li> | |
| <li> | |
| <code>-step=0</code> will use browser defaults for requestAnimationFrame frequency | |
| </li> | |
| <li> | |
| <code>-step=100</code> will request callback every 10 ms | |
| </li> | |
| <li> | |
| <code>-step=100:1000</code> will request callback every 10 ms, calling at most 1000 times the session<p></p> | |
| </li> | |
| </ul> | |
| <p class="threaded">When running without worker, internal threading of filters (e.g. ffmpeg & co) will be disabled if the session runs only on the main thread.</p> | |
| <p class="threaded">Use <code>-threads=N</code> with <code>N</code> > 0 (typically <code>-threads=1</code>) to enable threading in these filters.</p> | |
| </div> | |
| <script> | |
| const cmd_history = []; | |
| const statusElement = document.getElementById('status'); | |
| const progressTxt = document.getElementById('progress').childNodes[0]; | |
| const animElement = document.getElementById('logo_anim'); | |
| const runElement = document.getElementById('run'); | |
| const cancelElement = document.getElementById('cancel'); | |
| const flushElement = document.getElementById('flush'); | |
| const cmdElement = document.getElementById('cmd'); | |
| const reportElement = document.getElementById('reports'); | |
| const selCmdElement = document.getElementById('commands'); | |
| const outputElement = document.getElementById('output'); | |
| const abortElement = document.getElementById('abort'); | |
| const threadingElement = document.getElementById('threading'); | |
| let console_bck = ''; | |
| let cmd_idx = -1; | |
| let ctrl_down=false; | |
| let opt_down=false; | |
| let use_idbfs = false; | |
| let edit_name = null; | |
| let is_eval = false; | |
| let is_bsize = false; | |
| let is_custfs = false; | |
| let fsess = null; | |
| let prev_filter = null; | |
| let defer_mode = false; | |
| let logs_val = null; | |
| let suspend_sess = false; | |
| let fs_steps = 20; | |
| let FS = null; | |
| let STATIC_SRC; | |
| if (window.location.origin.indexOf('.gpac.io')>0) STATIC_SRC = `${window.location.origin}/data`; | |
| else if (window.location.origin.startsWith('http://')) STATIC_SRC = 'http://wasm.gpac.io/data'; | |
| else STATIC_SRC = 'https://wasm.gpac.io/data'; | |
| const examples = [ | |
| { | |
| "name": "media playback", | |
| "cmd": `gpac -graph -i ${STATIC_SRC}/video_180.mp4 -i ${STATIC_SRC}/audio.mp4 aout vout`, | |
| "info": '<a href="https://wiki.gpac.io/Player/filters-playback/">simple playback</a> of audio and video files. The <code>-graph</code> option prints out the internal graph showing all filters involved in the media processing pipeline.' | |
| }, | |
| { | |
| "name": "describe streams available in a media", | |
| "cmd": `gpac -i ${STATIC_SRC}/video_180.mp4 inspect:full`, | |
| "info": 'inspect a media and print its <a href="https://wiki.gpac.io/Filters/Rearchitecture/?h=pid#pid-description">PIDs</a> information, eg. information about the streams it contains.' | |
| }, | |
| { | |
| "name": "extract the first 2 seconds of a video", | |
| "cmd": `gpac -i ${STATIC_SRC}/video_180.mp4 reframer:xs=0:xe=T00:00:02 -o xs0-xe02.mp4`, | |
| "info": 'extract the first 2 seconds of a video.' | |
| }, | |
| { | |
| "name": "trick play: mux all IDR frames to a single MP4", | |
| "cmd": `gpac -logs=all@info -i ${STATIC_SRC}/video_180.mp4 @#Video reframer:saps=1 @ -o trick_play.mp4`, | |
| "info": "creates a video containing only the IDR frames (SAP=1) from the input source." | |
| }, | |
| { | |
| "name": "download remote DASH content", | |
| "cmd": `gpac -stats -i ${STATIC_SRC}/dash/bbb_30fps.mpd dashin:forward=file -o dump/$File$:dynext`, | |
| "info": null | |
| } | |
| ]; | |
| examples.forEach((el, i) => { | |
| if (el.cmd){ | |
| let opt = document.createElement("option"); | |
| opt.text = el.name; | |
| opt.value = i; | |
| selCmdElement.add(opt); | |
| } | |
| }); | |
| selCmdElement.onchange = () => { | |
| let idx = parseInt(selCmdElement.value); | |
| if (idx<0) return; | |
| let item = examples[idx]; | |
| setCommand(item.cmd.trim()); | |
| }; | |
| function setCommand(value) { | |
| cmdElement.value = value == '' ? null : value; | |
| }; | |
| function onGpacReady() { | |
| cmdElement.removeAttribute("disabled"); | |
| selCmdElement.removeAttribute("disabled"); | |
| runElement.removeAttribute("disabled"); | |
| cmdElement.setAttribute("placeholder", "Enter a command ..."); | |
| outputElement.setAttribute("aria-busy", "false"); | |
| outputElement.innerHTML += '<span class="version">GPAC' + LIBGPAC.version() + '</span><br><br>' + consolePrefix(); | |
| }; | |
| function toggle_eval_mode() { | |
| is_eval = !is_eval; | |
| let cmdDiv = document.getElementById('cmd_div'); | |
| if (is_eval){ | |
| outputElement.readonly = false; | |
| outputElement.setAttribute('contenteditable', true); | |
| outputElement.innerText = ''; | |
| set_status('Edit your JS and click run to evaluate, cancel to abort'); | |
| runElement.hidden = false; | |
| cancelElement.hidden = false; | |
| cmdDiv.hidden = true; | |
| selCmdElement.hidden = true; | |
| fsRunForm.hidden = true; | |
| reportElement.hidden = true; | |
| } else { | |
| outputElement.setAttribute('contenteditable', false); | |
| outputElement.readonly = true; | |
| runElement.hidden = false; | |
| cancelElement.hidden = true; | |
| cmdDiv.hidden = false; | |
| selCmdElement.hidden = false; | |
| fsRunForm.hidden = true; | |
| reportElement.hidden = false; | |
| } | |
| displayStatus(''); | |
| } | |
| function run_session() { | |
| let i=fs_steps; | |
| while (i) { | |
| let e = LIBGPAC.fs_run(fsess); | |
| if (e) break; | |
| i--; | |
| } | |
| if (suspend_sess || LIBGPAC.fs_last_task(fsess)) { | |
| animElement.stop(); | |
| if (!suspend_sess) set_status('Session done'); | |
| suspend_sess=false; | |
| } else { | |
| setTimeout(run_session, 0); | |
| } | |
| } | |
| //start anim on logo | |
| animElement.start = function() { | |
| if (! this.started) { | |
| this.setAttribute('dur', '2s'); | |
| this.beginElement(); | |
| this.started=true; | |
| } | |
| }; | |
| //stop anim on logo | |
| animElement.stop = function() { | |
| this.endElementAt(0); | |
| this.setAttribute('dur', 'indefinite'); | |
| this.started=false; | |
| }; | |
| runElement.onclick = function() { | |
| if (is_eval){ | |
| run_script(); | |
| } else { | |
| if (!GPAC.running){ | |
| if ( cmdElement.value.trim() == '' ) return; | |
| process_command(); | |
| setCommand(''); | |
| return; | |
| } | |
| if(GPAC.running){ | |
| GPAC.toggle_reporting(flushElement.value); | |
| } | |
| } | |
| } | |
| cancelElement.onclick = toggle_eval_mode | |
| //create missing dirs in an output path | |
| function check_file_dir(fname, is_directory) { | |
| let paths = fname.split('/'); | |
| let path ='/'; | |
| while (paths.length) { | |
| if (!paths[0].length) { | |
| paths.shift(); | |
| continue; | |
| } | |
| path += paths[0]; | |
| if ((paths.length==1) && !is_directory) break; | |
| if (paths.length>1) path+='/'; | |
| try { | |
| FS.mkdir(path); | |
| } catch(e) { | |
| } | |
| paths.shift(); | |
| } | |
| } | |
| //set status | |
| function set_status(str) | |
| { | |
| statusElement.innerHTML = str; | |
| } | |
| function do_log(str) { | |
| LIBGPAC.print(str); | |
| } | |
| function do_log_err(str) { | |
| LIBGPAC.printErr(str); | |
| } | |
| //load command history from /idbfs/.gpac/history.txt | |
| function load_history() { | |
| let s = FS.analyzePath('/idbfs', true); | |
| if (s.error) { | |
| set_status('IDBFS not mounted'); | |
| return; | |
| } | |
| use_idbfs = true; | |
| s = FS.analyzePath('/idbfs/.gpac', true); | |
| if (s.error) FS.mkdir('/idbfs/.gpac'); | |
| s = FS.analyzePath('/idbfs/.gpac/history.txt', true); | |
| if (s.error) return; | |
| let strs = FS.readFile('/idbfs/.gpac/history.txt', { encoding: "utf8"}).split('\n'); | |
| strs.forEach(s => { if (s.length && (cmd_history.indexOf(s)<0)) cmd_history.push(s); }); | |
| } | |
| //save command history to /idbfs/.gpac/history.txt | |
| function save_history() { | |
| if (!use_idbfs) { | |
| return; | |
| } | |
| let txt=''; | |
| cmd_history.forEach( c => { txt += c + '\n';}); | |
| FS.writeFile('/idbfs/.gpac/history.txt', txt); | |
| FS.syncfs(false, () => { | |
| if (arguments.length) { | |
| err('Failed to sync IDBFS: ' + arguments[0]); | |
| } | |
| }); | |
| } | |
| //runs a given script | |
| function run_script(cmd) { | |
| toggle_eval_mode(); | |
| var js_txt = outputElement.innerText; | |
| outputElement.removeAttribute('contenteditable'); | |
| outputElement.innerText = ''; | |
| set_status(''); | |
| err = ''; | |
| console.log('text is ' + js_txt); | |
| if (!cmd.length || (cmd=='go')) { | |
| var fun = Function(js_txt); | |
| try { | |
| fun(); | |
| } catch (e) { | |
| err = e.message; | |
| } | |
| } | |
| return com_exit(err); | |
| } | |
| function set_text(txt) | |
| { | |
| if (txt.length) outputElement.innerText += txt; | |
| else outputElement.innerText = ''; | |
| } | |
| //utility to log command error and append '/cwd/ %' at end of textArea | |
| function com_exit() { | |
| if (arguments.length && arguments[0].length) { | |
| do_log('Error: ' + arguments[0]); | |
| } | |
| GPAC.no_log = true; | |
| outputElement.innerHTML += `<br>${consolePrefix()}`; | |
| selCmdElement.hidden = false; | |
| fsRunForm.hidden = true; | |
| runElement.hidden = false; | |
| setCommand(''); | |
| } | |
| function onCommandRun(cmd) { | |
| // reset select example | |
| selCmdElement.value = -1; | |
| selCmdElement.hidden = true; | |
| runElement.hidden = true; | |
| fsRunForm.removeAttribute("hidden"); | |
| set_status(''); | |
| }; | |
| function expandFilePath(fp) { | |
| return fp.startsWith('/') ? fp : `${FS.cwd()}/${fp}` | |
| } | |
| //our basic terminal emulation | |
| function process_command() { | |
| const cmd = cmdElement.value; | |
| if (cmd.trim() == ''){ | |
| return; | |
| } | |
| onCommandRun(cmd); | |
| GPAC.no_log = false; | |
| args = cmd.trim().split(' '); | |
| //evaluates a script - turns textArea in write mode and wait for go/cancel | |
| if (args[0]=='eval') { | |
| toggle_eval_mode(); | |
| return; | |
| } | |
| //in eval mode, run | |
| if (is_eval) { | |
| run_script(cmd); | |
| return; | |
| } | |
| if (is_custfs) { | |
| run_prompt(cmd); | |
| setCommand(''); | |
| return; | |
| } | |
| if (is_bsize) { | |
| let s = parseInt(cmd); | |
| is_bsize = false; | |
| set_status(''); | |
| if (s != s) return com_exit('Inavlid value ' + cmd); | |
| GPAC.read_block_size = s; | |
| return com_exit(); | |
| } | |
| //nothing to do | |
| if (!cmd.length) { | |
| return com_exit(); | |
| } | |
| //in edit mode, save/discard content | |
| if (edit_name != null) { | |
| let do_save = false; | |
| if (args[0]=='save') { | |
| do_save = true; | |
| } else if (args[0] != 'close') { | |
| set_status('Use save or close to exit editor, not ' + args[0]); | |
| return; | |
| } | |
| outputElement.removeAttribute('contenteditable'); | |
| if (do_save) { | |
| FS.writeFile(edit_name, outputElement.innerText); | |
| } | |
| outputElement.innerHTML = ''; | |
| edit_name = null; | |
| return com_exit(); | |
| } | |
| //save history | |
| let cmd_idx = cmd_history.indexOf(cmd); | |
| if (cmd_idx>=0) { | |
| cmd_history.splice(cmd_idx, 1); | |
| } | |
| cmd_history.push(cmd); | |
| save_history(); | |
| cmd_idx = -1; | |
| //file or directory import | |
| if (args[0]=='imp') { | |
| let is_mdir = false; // import directory content | |
| let file_name = null; | |
| let is_dir = false; // import into directory ? | |
| args.shift(); | |
| if (args.length && (args[0]=='-d')) { | |
| is_mdir = true; | |
| args.shift(); | |
| } | |
| if (args.length){ | |
| file_name = args[0]; | |
| is_dir = (file_name.charAt(file_name.length-1) == '/'); | |
| if (is_mdir && !is_dir) { | |
| file_name += '/'; | |
| } | |
| file_name = expandFilePath(file_name); | |
| check_file_dir(file_name, is_dir); | |
| } | |
| if (is_mdir) | |
| is_dir=true; | |
| let input = document.createElement('input'); | |
| input['type'] = 'file'; | |
| if (!is_mdir) { | |
| input.multiple = is_dir; | |
| } else { | |
| input.directory = true; | |
| input.mozDirectory = true; | |
| input.webkitdirectory = true; | |
| } | |
| input.click(); | |
| input.onchange = function() { | |
| for (const file of event.target.files) { | |
| if (file.name.charAt(0) == '.') continue; | |
| let read = new FileReader(); | |
| let fname = file_name; //can be null | |
| if (is_dir) { | |
| if (is_mdir) { | |
| if (fname) { | |
| //strip dir name | |
| let name = file.webkitRelativePath.substring(file.webkitRelativePath.indexOf("/") + 1); | |
| fname += name; | |
| } else { | |
| //copy as is | |
| fname = file.webkitRelativePath; | |
| } | |
| } else { | |
| fname += file.name; | |
| } | |
| } | |
| if (!fname) fname = file.name; | |
| read.addEventListener('loadend', (e) => { | |
| const uint8_view = new Uint8Array(read.result); | |
| fname = expandFilePath(fname); | |
| if (is_mdir) check_file_dir(fname, false); | |
| FS.writeFile(fname, uint8_view); | |
| }); | |
| read.readAsArrayBuffer(file); | |
| } | |
| } | |
| return com_exit(); | |
| } | |
| //file or directory listing | |
| if (args[0]=='ls') { | |
| let list=0; | |
| if ((args.length>1) && (args[1]=='-l')) { | |
| list=1; | |
| args.splice(1, 1); | |
| } | |
| else if ((args.length>1) && (args[1]=='-la')) { | |
| list=2; | |
| args.splice(1, 1); | |
| } | |
| var d = ((args.length>1) && args[1].length) ? args[1] : FS.cwd(); | |
| try { | |
| let stats = FS.stat(d); | |
| if (FS.isDir(stats.mode)) { | |
| var children = FS.readdir(d, { parent: false }); | |
| //do_log('Contents of ' + d + ':'); | |
| children.forEach(i => { | |
| let text; | |
| if ((i=='.') || (i=='..')) return; | |
| text=i; | |
| if (list) { | |
| stats = FS.stat(d+'/'+i); | |
| if (list==1) { | |
| if (FS.isDir(stats.mode)) text += ' directory'; | |
| else text += ' size ' + stats.size; | |
| text += ' modified ' + stats.mtime.toUTCString(); | |
| } else { | |
| text = JSON.stringify(stats); | |
| } | |
| } | |
| do_log(text); | |
| }); | |
| } else { | |
| let text; | |
| if (list==2) text = JSON.stringify(stats); | |
| else { | |
| text = '' + stats.size + ' modified ' + stats.mtime; | |
| } | |
| do_log(text); | |
| } | |
| return com_exit(); | |
| } catch (e) { | |
| return com_exit('No such file'); | |
| } | |
| } | |
| //get current working directory | |
| if (args[0]=='cwd') { | |
| do_log(FS.cwd()); | |
| return com_exit(); | |
| } | |
| //change directory | |
| if (args[0]=='cd') { | |
| var d = (args.length>1) ? args[1] : '/home/web_user'; | |
| FS.chdir(d); | |
| return com_exit(); | |
| } | |
| //creates directory | |
| if (args[0]=='mkdir') { | |
| if (args.length>1) | |
| FS.mkdir(args[1]); | |
| return com_exit(); | |
| } | |
| //remove file | |
| if (args[0]=='rm') { | |
| if (args.length<2) return com_exit('Missing name'); | |
| let stats = FS.stat(args[1]); | |
| if (FS.isDir(stats.mode)) FS.rmdir(args[1]); | |
| else FS.unlink(args[1]); | |
| return com_exit(); | |
| } | |
| //rename file | |
| if (args[0]=='mv') { | |
| if (args.length<3) return com_exit('Missing file names'); | |
| let stats = FS.stat(args[1]); | |
| if (FS.isDir(stats.mode)) return com_exit(args[1] + ' is a directory'); | |
| FS.rename(args[1], args[2]); | |
| return com_exit(); | |
| } | |
| //copy file | |
| if (args[0]=='cp') { | |
| if (args.length<3) return com_exit('Missing file names'); | |
| let stats = FS.stat(args[1]); | |
| if (FS.isDir(stats.mode)) return com_exit(args[1] + ' is a directory'); | |
| FS.writeFile(args[2], FS.readFile(args[1], {encoding: "binary"})); | |
| return com_exit(); | |
| } | |
| //export file to user file system | |
| if (args[0]=='exp') { | |
| if (args.length<2) return com_exit('Missing file name'); | |
| let stats = FS.stat(args[1]); | |
| if (FS.isDir(stats.mode)) return com_exit(args[1] + ' is a directory'); | |
| var blob = new Blob([FS.readFile(args[1], {encoding: "binary"})], {type: "application/octet-stream"}); | |
| var a = document.createElement("a"); | |
| var url = window.URL.createObjectURL(blob); | |
| a.href = url; | |
| a.download = args[1]; | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| return com_exit(); | |
| } | |
| //show file | |
| if (args[0]=='show') { | |
| if (args.length<2) return com_exit('Missing file name'); | |
| let stats = FS.stat(args[1]); | |
| if (FS.isDir(stats.mode)) return com_exit(args[1] + ' is a directory'); | |
| let str = FS.readFile(args[1], { encoding: "utf8"}); | |
| do_log(str); | |
| return com_exit(); | |
| } | |
| //edit file | |
| if (args[0]=='edit') { | |
| if (args.length<2) return com_exit('Missing file name');; | |
| let stats = FS.stat(args[1]); | |
| if (FS.isDir(stats.mode)) return com_exit(args[1] + ' is a directory'); | |
| let str = FS.readFile(args[1], { encoding: "utf8"}); | |
| edit_name = args[1]; | |
| if (outputElement) { | |
| outputElement.setAttribute('contenteditable', true); | |
| outputElement.innerText = str; | |
| } | |
| return; | |
| } | |
| //worker tag set | |
| if (args[0]=='worker') { | |
| if (!LIBGPAC.has_threads) { | |
| LIBGPAC.gpac_worker=false; | |
| return com_exit('GPAC compiled without threads'); | |
| } | |
| LIBGPAC.gpac_worker = !LIBGPAC.gpac_worker; | |
| do_log('Worker Thread for main: ' + (LIBGPAC.gpac_worker ? 'on' : 'off') ); | |
| return com_exit(); | |
| } | |
| //print stack free size | |
| if (args[0]=='stack') { | |
| const get_stack_free = LIBGPAC.cwrap('emscripten_stack_get_free', 'number', []) | |
| do_log('Free stack: ' + get_stack_free() + ' bytes'); | |
| return com_exit(); | |
| } | |
| //clear console | |
| if (args[0]=='clear') { | |
| if (outputElement) outputElement.innerText = ''; | |
| return com_exit(); | |
| } | |
| //clear history | |
| if (args[0]=='clearh') { | |
| if (args.length>1) | |
| cmd_history = cmd_history.slice(- parseInt(args[1]) ); | |
| else | |
| cmd_history=[]; | |
| save_history(); | |
| return com_exit(); | |
| } | |
| //show allocated threads - usefull to check how many threads were indeed created by the previous run | |
| if (args[0]=='threads') { | |
| if (LIBGPAC.has_threads) { | |
| do_log('Threads Pool Size: ' + LIBGPAC['PThread'].unusedWorkers.length); | |
| return com_exit(); | |
| } | |
| return com_exit('GPAC compiled without threads'); | |
| } | |
| //worker tag set | |
| if (args[0]=='fetch') { | |
| LIBGPAC.gpac_fetch = !LIBGPAC.gpac_fetch; | |
| do_log('libgpac fetch() ' + (LIBGPAC.gpac_fetch ? 'on' : 'off') ); | |
| return com_exit(); | |
| } | |
| if (args[0]=='fs') { | |
| set_status('Starting interactive graph builder'); | |
| set_text(''); | |
| setCommand(''); | |
| defer_mode=true; | |
| fs_steps=20; | |
| is_custfs = true; | |
| return; | |
| } | |
| if (args[0]=='bsize') { | |
| set_status('Enter new block size to use (current ' + GPAC.read_block_size + ')'); | |
| is_bsize = true; | |
| return; | |
| } | |
| //set logs at runtime (doesn't work when video output is used, text input is always directed to SDL in this case...) | |
| if (args[0]=='logs') { | |
| if (!GPAC.running) { | |
| do_log('GPAC not running'); | |
| return; | |
| } | |
| if (args.length>1) { | |
| LIBGPAC.log_set_tools_levels(args[1], 1); | |
| } | |
| return; | |
| } | |
| // | |
| //end of commands | |
| // | |
| //run gpac or mp4box | |
| if (GPAC.running) { | |
| set_status('Already running, ignoring command'); | |
| return; | |
| } | |
| let will_block=false; | |
| let is_mp4box=false; | |
| if (args[0].toLowerCase()=='gpac') { | |
| args.shift(); | |
| } | |
| else if (args[0].toLowerCase()=='mp4box') { | |
| args.shift(); | |
| is_mp4box = true; | |
| if (LIBGPAC.gpac_worker==false) will_block=true; | |
| } | |
| //when using vout or compositor in standalone mode, don't use a worker thread since we run through emscripten_main_loop | |
| let use_video=false; | |
| let has_report = false; | |
| for (let i=0; i<args.length; i++) { | |
| a = args[i]; | |
| if (a.includes('vout') || a.includes('-play') || a.includes('-gui')) { | |
| use_video=true; | |
| } else if (a.includes('compositor')) { | |
| if (a.includes('base') || a.includes('gui')) use_video=true; | |
| } else if ((LIBGPAC.gpac_worker==false) && (a.startsWith('-step='))) { | |
| var val = parseInt(a.substring(6)); | |
| if (val<0) will_block = true; | |
| } | |
| else if (a.startsWith('-r')) { | |
| has_report = true; | |
| } | |
| //input file io | |
| else if (a=='-ib') { | |
| args[i] = '-i'; | |
| let f = args[i+1]; | |
| let fio = GFIO.create(f, 1); | |
| let wrap_url = fio.gfio_url(); | |
| args[i+1] = wrap_url; | |
| i++; | |
| } | |
| //output file io | |
| else if (a=='-ob') { | |
| args[i] = '-o'; | |
| let f = args[i+1]; | |
| let fio = GFIO.create(f, 2); | |
| let wrap_url = fio.gfio_url(); | |
| args[i+1] = wrap_url; | |
| i++; | |
| } | |
| //look for automated wrappers $(NAME), create fileIOs and replace names | |
| else { | |
| let select_start = a.indexOf('$('); | |
| let select_end = (select_start>=0) ? a.indexOf(')') : 0; | |
| if (select_end) { | |
| let hint = a.substring(select_start, select_end+1); | |
| let fio = GFIO.create(hint, 0); //can only use this for read mode at the time being | |
| let wrap_url = fio.gfio_url(); | |
| args[i] = a.replace(hint, wrap_url); | |
| } | |
| } | |
| } | |
| function run_prompt(cmd) { | |
| let args = cmd.split(' '); | |
| if (!args.length) return; | |
| if (args[0]=='q') { | |
| is_custfs=false; | |
| if (fsess) { | |
| LIBGPAC.fs_del(fsess); | |
| fsess=null; | |
| LIBGPAC.uninit(); | |
| } | |
| set_status("leaving interactive mode"); | |
| set_text(''); | |
| setCommand(''); | |
| return com_exit(); | |
| } | |
| if (args[0]=='d') { | |
| if (!fsess) { | |
| defer_mode = !defer_mode; | |
| set_status("will use defer linking mode: " + (defer_mode ? "yes" : "no")); | |
| } | |
| return; | |
| } | |
| if (args[0]=='l') { | |
| logs_val = args[1]; | |
| return; | |
| } | |
| if (args[0]=='s') { | |
| if (args.length>1) { | |
| fs_steps = parseInt(args[1]); | |
| if (fs_steps==0) fs_steps=1; | |
| if (fs_steps<0) | |
| set_status("will use blocking mode"); | |
| else | |
| set_status("will use " + fs_steps + " steps per callback"); | |
| } | |
| return; | |
| } | |
| if (args[0]=='a') { | |
| if (args.length<2) return set_status('Invalid add command'); | |
| if (!fsess) { | |
| let flag = 0; | |
| if (fs_steps>0) flag |= 1<<2; | |
| if (defer_mode) flag |= (1<<13); | |
| LIBGPAC.init(0, null); | |
| if (logs_val) | |
| LIBGPAC.log_set_tools_levels(logs_val, 1); | |
| fsess = LIBGPAC.fs_new_defaults(flag); | |
| set_status("Creating filter session in " + ((fs_steps<0) ? "" : "non-") + "blocking mode" + (defer_mode ? ' - deferred linking' : '') ); | |
| if (fs_steps<0) fs_steps=1; //for run_session | |
| } | |
| if (prev_filter && defer_mode) { | |
| LIBGPAC.filter_reconnect_output(prev_filter, null); | |
| prev_filter=null; | |
| } | |
| let fdesc = args[1]; | |
| let select_start = fdesc.indexOf('$('); | |
| let select_end = (select_start>=0) ? fdesc.indexOf(')') : 0; | |
| if (select_end) { | |
| let hint = fdesc.substring(select_start, select_end+1); | |
| let fio = GFIO.create(hint, 0); //can only use this for read mode at the time being | |
| let wrap_url = fio.gfio_url(); | |
| fdesc = fdesc.replace(hint, wrap_url); | |
| } | |
| const e = LIBGPAC._malloc(LIBGPAC.HEAP32.BYTES_PER_ELEMENT); | |
| LIBGPAC.HEAP32.set([0], e/LIBGPAC.HEAP32.BYTES_PER_ELEMENT); | |
| let f; | |
| if (fdesc.startsWith('src=')) { | |
| fdesc = fdesc.substring(4); | |
| f = LIBGPAC.fs_load_source(fsess, fdesc, null, null, e); | |
| } else if (fdesc.startsWith('dst=')) { | |
| fdesc = fdesc.substring(4); | |
| f = LIBGPAC.fs_load_destination(fsess, fdesc, null, null, e); | |
| } else { | |
| f = LIBGPAC.fs_load_filter(fsess, fdesc, e); | |
| } | |
| prev_filter = f; | |
| let res = LIBGPAC.getValue(e, 'i32'); | |
| LIBGPAC._free(e); | |
| set_status("added filter "+args[1]+ " returned: " + LIBGPAC.e2s(res)); | |
| return; | |
| } | |
| if (args[0]=='r') { | |
| if (!fsess) return set_status('No valid session, add a filter first'); | |
| animElement.start(); | |
| run_session(); | |
| return; | |
| } | |
| if (args[0]=='c') { | |
| if (fsess) { | |
| suspend_sess = true; | |
| set_status("Suspending session"); | |
| } | |
| return; | |
| } | |
| if (args[0]=='clear') { | |
| if (outputElement) outputElement.innerText = ''; | |
| return; | |
| } | |
| if (args[0]=='p') { | |
| let idx=0; | |
| let n; | |
| if (!fsess) return set_status('No valid session, add a filter first'); | |
| if (!prev_filter) return set_status('No filter loaded, cannot probe'); | |
| if (args.length<2) return set_status('No name provided'); | |
| if (args.length>2) { | |
| idx=parseInt(args[1]); | |
| n=args[2]; | |
| } else { | |
| n=args[1]; | |
| } | |
| var p_str = LIBGPAC._malloc(LIBGPAC.HEAP32.BYTES_PER_ELEMENT); | |
| var res = LIBGPAC.filter_probe_link(prev_filter, idx, n, p_str); | |
| if (!res) { | |
| var c_str = LIBGPAC.getValue(p_str, "i32"); | |
| var chain = LIBGPAC.UTF8ToString(c_str); | |
| LIBGPAC.free(c_str); | |
| if (chain.length) | |
| set_text('resolved chain from ' + LIBGPAC.filter_get_name(prev_filter) + ' to ' + n + ' is: ' + chain); | |
| else | |
| set_text('Direct connection from from ' + LIBGPAC.filter_get_name(prev_filter) + ' to ' + n + ' is possible'); | |
| } | |
| LIBGPAC._free(p_str); | |
| return set_status(res ? ('Failed: '+ LIBGPAC.e2s(res) ) : 'OK'); | |
| } | |
| if (args[0]=='l') { | |
| let idx=-1; | |
| let n; | |
| if (!fsess) return set_status('No valid session, add a filter first'); | |
| if (!prev_filter) return set_status('No filter loaded, cannot check destinations'); | |
| if (args.length>1) { | |
| idx=parseInt(args[1]); | |
| } | |
| var p_str = LIBGPAC._malloc(LIBGPAC.HEAP32.BYTES_PER_ELEMENT); | |
| var res = LIBGPAC.filter_get_destinations(prev_filter, idx, p_str); | |
| if (!res) { | |
| var c_str = LIBGPAC.getValue(p_str, "i32"); | |
| var chain = LIBGPAC.UTF8ToString(c_str); | |
| LIBGPAC.free(c_str); | |
| set_text('possible destinations for ' + LIBGPAC.filter_get_name(prev_filter) +': ' + chain); | |
| } | |
| LIBGPAC._free(p_str); | |
| return set_status(res ? ('Failed'+res) : 'OK'); | |
| } | |
| if (args[0]=='v') { | |
| if (!fsess) return set_status('No valid session, add a filter first'); | |
| LIBGPAC.fs_print_stats(fsess); | |
| LIBGPAC.fs_print_connections(fsess); | |
| return; | |
| } | |
| if (args[0]=='i') { | |
| if (!prev_filter) return set_status('No current fiter, add a filter first'); | |
| txt='Filter ' + LIBGPAC.filter_get_name(prev_filter) + ' PID info:\n'; | |
| let c_i = LIBGPAC.filter_get_ipid_count(prev_filter); | |
| if (c_i) { | |
| for (let idx=0; idx<c_i; idx++) { | |
| let pid = LIBGPAC.filter_get_ipid(prev_filter, idx); | |
| txt+='Input PID ' + (idx+1) + ' Properties:\n'; | |
| let props = LIBGPAC.filter_pid_get_props(pid); | |
| txt+=JSON.stringify(props); | |
| txt+='\n\n'; | |
| } | |
| } | |
| let c_o = LIBGPAC.filter_get_opid_count(prev_filter); | |
| if (c_o) { | |
| for (let idx=0; idx<c_o; idx++) { | |
| let pid = LIBGPAC.filter_get_opid(prev_filter, idx); | |
| txt+='Output PID ' + (idx+1) + ' Properties:\n'; | |
| let props = LIBGPAC.filter_pid_get_props(pid); | |
| txt+=JSON.stringify(props); | |
| txt+='\n\n'; | |
| } | |
| } | |
| if (!c_i && !c_o) txt+='No PIDs - run session'; | |
| set_text(txt); | |
| return; | |
| } | |
| set_status("Unknown command "+cmd); | |
| let help="Possible commands:\n"; | |
| help+= "- d: set defer linking mode (default: "+ defer_mode + ")\n"; | |
| help+= "- s: set number of steps before returning control to JS, use -1 for blocking mode (default: "+ fs_steps + ")\n"; | |
| help+= "- l LT: set log tools and levels, same syntax as -logs\n"; | |
| help+= "- a DESC: add filter, use src=URL for source, dst=URL for sinks. $(name) replacement rules are allowed\n"; | |
| help+= "- r: run or resume session\n"; | |
| help+= "- c: suspend session while running\n"; | |
| help+= "- v: view connections and stats\n"; | |
| help+= "- q: exit custom mode\n"; | |
| help+= "\nThe following only apply to the last added filter:\n"; | |
| help+= "- i: print pid properties\n"; | |
| help+= "- p [pid_idx=0] DESC: probe resolution from pid with index pid_idx of last added filter to filter description DESC\n"; | |
| help+= "- l [pid_idx=-1]: list possible destinations for pid of index pid_idx (-1 for all pids) of last added filter\n"; | |
| help+= "\n"; | |
| set_text(help); | |
| } | |
| let controlElement = document.getElementById('controls'); | |
| // controlElement.hidden = is_mp4box ? true : false; | |
| // controlElement.style.display = is_mp4box ? 'none' : 'inline-block'; | |
| flushElement.selectedIndex = 0; | |
| reportElement.checked = has_report; | |
| let fsElement = document.getElementById('fullscreen'); | |
| fsElement.hidden = use_video ? false : true; | |
| GPAC.running = true; | |
| set_status('Running ' + (is_mp4box ? "MP4Box " : "gpac ") + cmd); | |
| animElement.start(); | |
| if (use_video && LIBGPAC.gpac_worker) { | |
| LIBGPAC.gpac_worker = false; | |
| GPAC.restore_worker = true; | |
| //cannot run SDL + main_loop in thread unless for offscreen canvas | |
| do_log('Main video output used, disabling worker mode for this run'); | |
| } | |
| //our own version of callMain to deal with: | |
| //- stack save/restore missing in emscripten callMain | |
| //- even if it where there, we may exit aynchronously and can only restore stack on main thread in gpac.on_done | |
| function call_gpac() { | |
| GPAC.stack = LIBGPAC.stackSave(); | |
| args.unshift(is_mp4box ? "MP4Box" : "gpac"); | |
| var argc = args.length; | |
| var argv = LIBGPAC.stringsToPtr(args); | |
| try { | |
| LIBGPAC["_main"](argc, argv); | |
| } catch (e) { | |
| //unwind thrown by emscripten main | |
| if (e != 'unwind') { | |
| GPAC.on_done(); | |
| return com_exit('Exception while running - ' + e); | |
| } | |
| } | |
| }; | |
| if (will_block) { | |
| let warn='Worker for main disabled - web page will be blocked!'; | |
| do_log(warn); | |
| set_status(warn); | |
| //use setTimeout to have time to display above warning... | |
| setTimeout(call_gpac, 1); | |
| } else { | |
| call_gpac(); | |
| } | |
| } | |
| //end of command line processing | |
| // Include ANSI escape codes convert | |
| // @link: https://www.npmjs.com/package/ansi-html | |
| // Reference to https://github.com/sindresorhus/ansi-regex | |
| var _regANSI = /(?:(?:\u001b\[)|\u009b)(?:(?:[0-9]{1,3})?(?:(?:;[0-9]{0,3})*)?[A-M|f-m])|\u001b[A-M]/ | |
| var _defColors = { | |
| reset: ['fff', '000'], // [FOREGROUD_COLOR, BACKGROUND_COLOR] | |
| black: '000', | |
| red: 'ff0000', | |
| green: '209805', | |
| yellow: 'e8bf03', | |
| blue: '0000ff', | |
| magenta: 'ff00ff', | |
| cyan: '00ffee', | |
| lightgrey: 'f0f0f0', | |
| darkgrey: '888' | |
| } | |
| var _styles = { | |
| 30: 'black', | |
| 31: 'red', | |
| 32: 'green', | |
| 33: 'yellow', | |
| 34: 'blue', | |
| 35: 'magenta', | |
| 36: 'cyan', | |
| 37: 'lightgrey' | |
| } | |
| var _openTags = { | |
| '1': 'font-weight:bold', // bold | |
| '2': 'opacity:0.5', // dim | |
| '3': '<i>', // italic | |
| '4': '<u>', // underscore | |
| '8': 'display:none', // hidden | |
| '9': '<del>' // delete | |
| } | |
| var _closeTags = { | |
| '23': '</i>', // reset italic | |
| '24': '</u>', // reset underscore | |
| '29': '</del>' // reset delete | |
| } | |
| ;[0, 21, 22, 27, 28, 39, 49].forEach(function (n) { | |
| _closeTags[n] = '</span>' | |
| }) | |
| /** | |
| * Converts text with ANSI color codes to HTML markup. | |
| * @param {String} text | |
| * @returns {*} | |
| */ | |
| function ansiHTML (text) { | |
| // Returns the text if the string has no ANSI escape code. | |
| if (!_regANSI.test(text)) { | |
| return text | |
| } | |
| // Cache opened sequence. | |
| var ansiCodes = [] | |
| // Replace with markup. | |
| var ret = text.replace(/\033\[(\d+)m/g, function (match, seq) { | |
| if (seq=='0') { | |
| r = ''; | |
| ansiCodes.forEach( c => { | |
| if (c == '3') r+='</i>'; | |
| else if (c == '4') r+='</b>'; | |
| }); | |
| ansiCodes.length=0; | |
| return r+'</span>'; | |
| } | |
| var ot = _openTags[seq] | |
| if (ot) { | |
| // If current sequence has been opened, close it. | |
| if (!!~ansiCodes.indexOf(seq)) { // eslint-disable-line no-extra-boolean-cast | |
| ansiCodes.pop() | |
| return '</span>' | |
| } | |
| // Open tag. | |
| ansiCodes.push(seq) | |
| return ot[0] === '<' ? ot : '<span style="' + ot + ';">' | |
| } | |
| var ct = _closeTags[seq] | |
| if (ct) { | |
| // Pop sequence | |
| ansiCodes.pop() | |
| return ct | |
| } | |
| return '' | |
| }) | |
| // Make sure tags are closed. | |
| var l = ansiCodes.length | |
| ;(l > 0) && (ret += Array(l + 1).join('</span>')) | |
| return ret | |
| } | |
| /** | |
| * Customize colors. | |
| * @param {Object} colors reference to _defColors | |
| */ | |
| ansiHTML.setColors = function (colors) { | |
| if (typeof colors !== 'object') { | |
| throw new Error('`colors` parameter must be an Object.') | |
| } | |
| var _finalColors = {} | |
| for (var key in _defColors) { | |
| var hex = colors.hasOwnProperty(key) ? colors[key] : null | |
| if (!hex) { | |
| _finalColors[key] = _defColors[key] | |
| continue | |
| } | |
| if ('reset' === key) { | |
| if (typeof hex === 'string') { | |
| hex = [hex] | |
| } | |
| if (!Array.isArray(hex) || hex.length === 0 || hex.some(function (h) { | |
| return typeof h !== 'string' | |
| })) { | |
| throw new Error('The value of `' + key + '` property must be an Array and each item could only be a hex string, e.g.: FF0000') | |
| } | |
| var defHexColor = _defColors[key] | |
| if (!hex[0]) { | |
| hex[0] = defHexColor[0] | |
| } | |
| if (hex.length === 1 || !hex[1]) { | |
| hex = [hex[0]] | |
| hex.push(defHexColor[1]) | |
| } | |
| hex = hex.slice(0, 2) | |
| } else if (typeof hex !== 'string') { | |
| throw new Error('The value of `' + key + '` property must be a hex string, e.g.: FF0000') | |
| } | |
| _finalColors[key] = hex | |
| } | |
| _setTags(_finalColors) | |
| } | |
| /** | |
| * Reset colors. | |
| */ | |
| ansiHTML.reset = function () { | |
| _setTags(_defColors) | |
| } | |
| /** | |
| * Expose tags, including open and close. | |
| * @type {Object} | |
| */ | |
| ansiHTML.tags = {} | |
| if (Object.defineProperty) { | |
| Object.defineProperty(ansiHTML.tags, 'open', { | |
| get: function () { return _openTags } | |
| }) | |
| Object.defineProperty(ansiHTML.tags, 'close', { | |
| get: function () { return _closeTags } | |
| }) | |
| } else { | |
| ansiHTML.tags.open = _openTags | |
| ansiHTML.tags.close = _closeTags | |
| } | |
| function _setTags (colors) { | |
| // reset all | |
| _openTags['0'] = 'font-weight:normal;opacity:1;color:#' + colors.reset[0] + ';background:#' + colors.reset[1] | |
| // inverse | |
| _openTags['7'] = 'color:#' + colors.reset[1] + ';background:#' + colors.reset[0] | |
| // dark grey | |
| _openTags['90'] = 'color:#' + colors.darkgrey | |
| for (var code in _styles) { | |
| var color = _styles[code] | |
| var oriColor = colors[color] || '000' | |
| _openTags[code] = 'color:#' + oriColor | |
| code = parseInt(code) | |
| _openTags[(code + 10).toString()] = 'background:#' + oriColor | |
| } | |
| } | |
| ansiHTML.reset() | |
| // | |
| //Until end of this script: GFIO wrapper | |
| // | |
| //This demo comes with a GF_FileIO wrapper for | |
| // - Emscripten FS file access | |
| // - User file system access | |
| // - protocols supported by Fetch() API | |
| // | |
| // It is not optimized but can help you design your own fileIOs for custom GPAC@WASM integration | |
| const consolePrefix = () => { | |
| return "<span class=\"cwd\">"+FS.cwd()+"</span>$ "; | |
| }; | |
| //check support for fetch upload using readable stream | |
| const supportsRequestStreams = (() => { | |
| let duplexAccessed = false; | |
| const hasContentType = new Request('', { | |
| body: new ReadableStream(), | |
| method: 'POST', | |
| get duplex() { | |
| duplexAccessed = true; | |
| return 'half'; | |
| }, | |
| }).headers.has('Content-Type'); | |
| return duplexAccessed && !hasContentType; | |
| })(); | |
| //our GFIO object | |
| var GFIO = { | |
| //all defined GF_IOs | |
| dict: [], | |
| //constants used by gf_fileio_set_write_state | |
| WRITE_READY: 0, | |
| WRITE_WAIT: 1, | |
| WRITE_CANCEL: 2, | |
| //default method used for upload | |
| UPLOAD_METHOD: 'POST', | |
| //allow defer upload of file (aggregates to mem then basic POST/PUT or ar->blob->download) | |
| ALLOW_WRITE_DEFER: true, | |
| //open for read a file on user disk (input['type']='file') | |
| _open_read_local: function (fio) { | |
| let input = document.createElement('input'); | |
| input['type'] = 'file'; | |
| //no cancel handler, set a timer to 20s | |
| fio._timer = window.setTimeout( ()=> { | |
| if (!fio._select) return; | |
| set_status('timeout on file selection for ' + fio.url); | |
| fio._select = 2; | |
| fio._timer = null; | |
| }, 20000); | |
| input.addEventListener("change", e => { | |
| set_status(''); | |
| if (e.target.files[0]) { | |
| fio._file = e.target.files[0]; | |
| fio._select = 0; | |
| fio._pos = 0; | |
| fio._size = fio._file.size; | |
| //if more than 1G, load by 10M blocks | |
| if ((fio._size>1000000000) && (fio._max_bsize<10000000)) fio._max_bsize = 10000000; | |
| //if more than 1m, load by 500k blocks | |
| else if ((fio._size>1000000) && (fio._max_bsize<500000)) fio._max_bsize = 500000; | |
| GFIO.set_stats(fio._gfio, BigInt(0), BigInt(fio._size), 2, 0); | |
| GFIO._load_block(fio); | |
| } else { | |
| _fio._select = 2; | |
| } | |
| window.clearTimeout(fio._timer); | |
| fio._timer = null; | |
| }); | |
| input.click(); | |
| set_status('Select input file for ' + fio.url); | |
| return LIBGPAC.OK; | |
| }, | |
| //open for write a file on user disk - showSaveFilePicker->createWitable, or blob->download if not supported | |
| _open_write_local: function (fio) { | |
| if (typeof window.showSaveFilePicker == 'undefined') { | |
| if (GFIO.ALLOW_WRITE_DEFER) { | |
| fio._defer_write = new Uint8Array(); | |
| fio._select = 0; | |
| return LIBGPAC.OK; | |
| } | |
| do_log_err('File System Access API not supported'); | |
| return LIBGPAC.NOT_SUPPORTED; | |
| } | |
| //create file writer | |
| GFIO.set_ready(fio._gfio, GFIO.WRITE_WAIT); | |
| set_status('Select output file for ' + fio.url); | |
| fio._writer = null; | |
| const options = { suggestedName: fio.url, types: [ { description: fio.url, accept: { '*/*': [], } } ] }; | |
| window.showSaveFilePicker(options).then( filep => { | |
| return filep.createWritable(); | |
| }).then( writer => { | |
| fio._writer = writer; | |
| fio._select = 0; | |
| GFIO.set_ready(fio._gfio, GFIO.WRITE_READY); | |
| }).catch(val => { | |
| GFIO.set_ready(fio._gfio, GFIO.WRITE_CANCEL); | |
| fio._select = 2; | |
| }); | |
| return LIBGPAC.OK; | |
| }, | |
| //open a remote file (fetch) | |
| _open_read_remote: function (fio) { | |
| let res = LIBGPAC.OK; | |
| fio._controller = new AbortController(); | |
| let options = { | |
| signal: fio._controller.signal, | |
| mode: 'no-cors', | |
| }; | |
| options.headers = {}; | |
| if (fio._pos) { | |
| options.headers['range'] = 'bytes='+ fio._pos+'-'; | |
| } | |
| options.headers['cross-origin-embedder-policy'] = 'credentialless'; | |
| options.headers['cross-origin-resource-policy'] = 'cross-origin'; | |
| GFIO.set_stats(fio._gfio, BigInt(0), BigInt(0), 2, 0); | |
| fetch(fio.url, options).then((response) => { | |
| if (response.ok) { | |
| fio._select=0; | |
| fio._fetch_wait=true; | |
| fio._fetch = response.body.getReader(); | |
| fio._size = response.headers.get('Content-Length') || 0; | |
| GFIO.set_stats(fio._gfio, BigInt(0), BigInt(fio._size), 2, 0); | |
| } else { | |
| GFIO.set_ready(fio._gfio, GFIO.WRITE_CANCEL); | |
| fio._select=2; | |
| } | |
| }) | |
| .catch( (e) => { | |
| fio._select=2; | |
| GFIO.set_ready(fio._gfio, GFIO.WRITE_CANCEL); | |
| res = LIBGPAC.URL_ERROR; | |
| do_log_err('Fetch error: ' + e); | |
| }); | |
| return res; | |
| }, | |
| //write a remote file (fetch with readableStream if supported or fetch at end of file) | |
| _open_write_remote: function (fio) { | |
| let res = LIBGPAC.OK; | |
| fio._defer_write = null; | |
| if (!GFIO.supportsRequestStreams) { | |
| if (GFIO.ALLOW_WRITE_DEFER) { | |
| fio._defer_write = new Uint8Array(); | |
| fio._select=0; | |
| return LIBGPAC.OK; | |
| } | |
| do_log_err('Fetch upload with readable streams not supported'); | |
| return LIBGPAC.NOT_SUPPORTED; | |
| } | |
| fio._controller = new AbortController(); | |
| GFIO.set_ready(fio._gfio, GFIO.WRITE_WAIT); | |
| fio._upstream_dst=null; | |
| fio._upstream = new ReadableStream({ | |
| start(controller) { | |
| fio._select=0; | |
| fio._upstream_dst = controller; | |
| GFIO.set_ready(fio._gfio, GFIO.WRITE_READY); | |
| }, | |
| pull(controller) { | |
| fio._upstream_dst = controller; | |
| }, | |
| cancel() { | |
| GFIO.set_ready(fio._gfio, GFIO.WRITE_CANCEL); | |
| } | |
| }); | |
| let options = { | |
| method: GFIO.UPLOAD_METHOD, | |
| signal: fio._controller.signal, | |
| body: fio._upstream, | |
| duplex: 'half', | |
| headers: { 'Content-Type': 'application/octet-stream' }, | |
| AllowHTTP1ForStreamingUpload: true | |
| }; | |
| if (fio._pos) { | |
| options.headers = { | |
| 'range': 'bytes='+ fio._pos+'-' | |
| }; | |
| } | |
| fetch(fio.url, options).then((response) => { | |
| fio._select=0; | |
| fio._size = fio._pos; | |
| do_log('PUT got response ' + response.ok); | |
| }) | |
| .catch( (e) => { | |
| if (GFIO.ALLOW_WRITE_DEFER) { | |
| fio._defer_write = new Uint8Array(); | |
| fio._select=0; | |
| res = LIBGPAC.OK; | |
| fio._upstream = null; | |
| fio._upstream_dst = null; | |
| fio._controller = null; | |
| GFIO.set_ready(fio._gfio, GFIO.WRITE_READY); | |
| } else { | |
| GFIO.set_ready(fio._gfio, GFIO.WRITE_CANCEL); | |
| res = LIBGPAC.NOT_SUPPORTED; | |
| } | |
| }); | |
| return res; | |
| }, | |
| _close: function(fio) { | |
| if (fio._stream) FS.close(fio._stream); | |
| if (fio._writer) fio._writer.close(); | |
| if (fio._fetch) { | |
| fio._controller.abort(); | |
| fio._controller = null; | |
| } | |
| if (fio._defer_write) { | |
| if ((fio.url.indexOf('://')>0) && !fio.url.startsWith('file://')) { | |
| let options = { | |
| method: GFIO.UPLOAD_METHOD, | |
| body: fio._defer_write, | |
| headers: { 'Content-Type': 'application/octet-stream' }, | |
| }; | |
| if (fio._pos&&0) { | |
| options.headers = { | |
| 'range': 'bytes='+ fio._pos+'-' | |
| }; | |
| } | |
| fetch(fio.url, options).then((response) => { | |
| if (!response.ok) do_log('Error uploading ' + fio.url); | |
| }) | |
| .catch( (e) => { | |
| do_log('Error uploading ' + fio.url + ': ' + e); | |
| }); | |
| } else { | |
| var blob = new Blob([fio._defer_write.buffer], {type: 'application/octet-stream'}); | |
| var a = document.createElement("a"); | |
| var blob_url = window.URL.createObjectURL(blob); | |
| a.href = blob_url; | |
| a.download = fio.url; | |
| a.click(); | |
| window.URL.revokeObjectURL(blob_url); | |
| } | |
| } | |
| fio._stream = null; | |
| fio._writer = null; | |
| fio._fetch = null; | |
| fio._defer_write = null; | |
| fio._upstream = null; | |
| fio._pos = 0; | |
| }, | |
| //open callback from libgpac for GF_FileIO objects | |
| open: function(fileio_ref, _url, _mode, out_e) { | |
| let fio = GFIO.to_obj(fileio_ref); | |
| let url = _url ? LIBGPAC.UTF8ToString(_url) : null; | |
| let mode = LIBGPAC.UTF8ToString(_mode); | |
| //close file | |
| if (mode=='close') { | |
| GFIO._close(fio); | |
| LIBGPAC.setValue(out_e, LIBGPAC.OK, 'i32'); | |
| return 0; | |
| } | |
| //checks if file exists - we only probe for EM's FS for now | |
| if (mode=='probe') { | |
| let rurl = url.startsWith('gfio://') ? fio.url : url; | |
| try { | |
| let stats = FS.stat(rurl); | |
| LIBGPAC.setValue(out_e, LIBGPAC.OK, 'i32'); | |
| } catch (e) { | |
| LIBGPAC.setValue(out_e, LIBGPAC.URL_ERROR, 'i32'); | |
| } | |
| return 0; | |
| } | |
| //add ref to this object | |
| if (mode=='ref') { | |
| fio._refs++; | |
| return fileio_ref; | |
| } | |
| //remove ref from this object | |
| if (mode=='unref') { | |
| if (!fio._refs) return null; | |
| fio._refs--; | |
| if (fio._refs) return fileio_ref; | |
| url = null; | |
| } | |
| //special mode URL to create a new GFIO from parent file name but without creating the file (used in dashing) | |
| if (mode=="url") { | |
| if (!url) return null; | |
| let _newurl = LIBGPAC.url_cat(fio.url, url); | |
| var new_gfio = GFIO.create( LIBGPAC.UTF8ToString(_newurl), 0); | |
| LIBGPAC.free(newurl); | |
| return new_gfio._gfio; | |
| } | |
| //no URL, close file | |
| if (!url) { | |
| GFIO._close(fio); | |
| //TODO, cleanup of unused gfio refs ? | |
| //if (!fio._refs) {} | |
| return null; | |
| } | |
| mode = mode.replace('b', ''); | |
| mode = mode.replace('t', ''); | |
| mode = mode.replace('+', ''); | |
| //we don't support append modes | |
| if ((mode != "r") && (mode != "w")) { | |
| LIBGPAC.setValue(out_e, LIBGPAC.NOT_SUPPORTED, 'i32'); | |
| set_status('Unsupported open mode ' + mode); | |
| return null; | |
| } | |
| //file handle not opened, we can use the current gfio | |
| if (!fio._stream && !fio._file && !fio._fetch && !fio._upstream | |
| && (url.startsWith("gfio://") || (url == fio.url)) | |
| ) { | |
| var res=LIBGPAC.OK; | |
| fio._pos = 0; | |
| // URL is NOT an EM's FS file | |
| if (fio._wrap_io) { | |
| fio._select = 1; | |
| if (mode=='r') { | |
| if ((fio.url.indexOf('://')>0) && !fio.url.startsWith('file://')) { | |
| res = GFIO._open_read_remote(fio); | |
| } else { | |
| res = GFIO._open_read_local(fio); | |
| } | |
| } | |
| else if (mode=='w') { | |
| if ((fio.url.indexOf('://')>0) && !fio.url.startsWith('file://')) { | |
| res = GFIO._open_write_remote(fio); | |
| } else { | |
| res = GFIO._open_write_local(fio); | |
| } | |
| } | |
| } | |
| // URL is an EM's FS file | |
| else { | |
| try { | |
| fio._stream = FS.open(fio.url, mode); | |
| if (mode.startsWith('r')) { | |
| let stats = FS.stat(fio.url); | |
| fio._size = stats.size; | |
| GFIO.set_stats(fio._gfio, BigInt(stats.size), BigInt(stats.size), 1, 0); | |
| } | |
| } catch (e) { | |
| res = LIBGPAC.URL_ERROR; | |
| } | |
| } | |
| LIBGPAC.setValue(out_e, res, 'i32'); | |
| if (res != LIBGPAC.OK) return null; | |
| //return ourself | |
| return fio._gfio; | |
| } | |
| //file handle already open (file is being opened twice), create a new gfio or check if we have already created one | |
| //we only support this for local files | |
| if (!fio._stream) { | |
| LIBGPAC.setValue(out_e, LIBGPAC.NOT_SUPPORTED, 'i32'); | |
| return null; | |
| } | |
| let fio_ref = fio; | |
| fio = null; | |
| let no_concatenate=false; | |
| GFIO.dict.forEach(a_gfio => { | |
| if (fio) return; | |
| if (a_gfio.url != url) return; | |
| if (a_gfio._stream) { | |
| no_concatenate = true; | |
| } else { | |
| fio = a_gfio; | |
| } | |
| }); | |
| if (!fio) { | |
| let _path = null; | |
| let path = null; | |
| if (!url.startsWith("gfio://")) { | |
| if (no_concatenate) { | |
| path = url; | |
| } else { | |
| _path = LIBGPAC.url_cat(fio_ref.url, url); | |
| path = LIBGPAC.UTF8ToString(_path); | |
| } | |
| } else { | |
| path = fio_ref.url; | |
| } | |
| let res=LIBGPAC.OK; | |
| try { | |
| fio = GFIO.create(path, 0); | |
| } catch (e) { | |
| res = LIBGPAC.OUT_OF_MEM; | |
| } | |
| if (_path) LIBGPAC.free(_path); | |
| LIBGPAC.setValue(out_e, res, 'i32'); | |
| if (res) return null; | |
| } | |
| try { | |
| if (!url.startsWith("gfio://")) { | |
| fio._stream = FS.open(fio.url, mode); | |
| } else { | |
| fio._stream = FS.open(fio_ref.url, mode); | |
| } | |
| } catch (e) { | |
| //todo cleanup of old gfios | |
| LIBGPAC.setValue(out_e, LIBGPAC.IO_ERR, 'i32'); | |
| return null; | |
| } | |
| if (mode.startsWith('r')) { | |
| let stats = FS.stat(fio.url); | |
| fio._size = stats.size; | |
| GFIO.set_stats(fio._gfio, BigInt(stats.size), BigInt(stats.size), 1, 0); | |
| } | |
| return fio._gfio; | |
| }, | |
| //seek callback from libgpac | |
| seek: function(fileio, offset, whence) { | |
| let fio = GFIO.to_obj(fileio); | |
| if (fio._select==1) return LIBGPAC.OK; | |
| if (fio._defer_write) return LIBGPAC.NOT_SUPPORTED; | |
| if (fio._file || fio._writer || fio._fetch) { | |
| let abspos = Number(offset); | |
| if (whence==1) abspos += fio._pos; | |
| else if (whence==2) abspos = fio._size; | |
| if (fio._writer) { | |
| fio._pos = abspos; | |
| fio._writer.seek(abspos); | |
| return LIBGPAC.OK; | |
| } | |
| if (abspos != fio._pos) { | |
| fio._discard_next = true; | |
| fio._pos = abspos; | |
| fio._block_pos_in_file = abspos; | |
| } | |
| return LIBGPAC.OK; | |
| } | |
| try { | |
| fio._pos = FS.llseek(fio._stream, Number(offset), whence); | |
| } catch (e) { | |
| return LIBGPAC.IO_ERR; | |
| } | |
| return LIBGPAC.OK; | |
| }, | |
| //read callback from libgpac | |
| read: function(fileio, buffer, bytes) { | |
| let fio = GFIO.to_obj(fileio); | |
| //IO being created, wait | |
| if (fio._select) return 0; | |
| //file system stream or fetch | |
| if (fio._file || fio._fetch) { | |
| let load_next=false; | |
| if (bytes>fio._max_bsize) fio._max_bsize = bytes; | |
| if (!fio._ab) { | |
| //fetch | |
| if (fio._fetch_wait) { | |
| fio._fetch_wait = false; | |
| fio._fetch.read().then( block => { | |
| if (block.done) { | |
| //done, force size in case it was not set | |
| fio._size = fio._pos; | |
| } else { | |
| fio._ab = block.value; fio._block_pos = 0; | |
| } | |
| }); | |
| } | |
| return 0; | |
| } | |
| if (fio._discard_next) { | |
| fio._ab = null; | |
| fio._block_pos = 0; | |
| fio._discard_next = 0; | |
| if (fio._fetch) { | |
| fio._controller.abort(); | |
| fio._controller = null; | |
| GFIO._open_read_remote(fio); | |
| } else { | |
| GFIO._load_block(fio); | |
| } | |
| return 0; | |
| } | |
| let remain = fio._ab.byteLength - fio._block_pos; | |
| if (bytes > remain) { | |
| load_next = true; | |
| bytes = remain; | |
| } | |
| //copy array buffer | |
| let src; | |
| if (fio._fetch) | |
| src = fio._ab.subarray(fio._block_pos, fio._block_pos+bytes); | |
| else | |
| src = new Uint8Array(fio._ab, fio._block_pos, bytes); | |
| let dst = new Uint8Array(LIBGPAC.HEAPU8.buffer, buffer, bytes); | |
| dst.set(src); | |
| fio._pos += bytes; | |
| if (load_next) { | |
| fio._block_pos_in_file += fio._ab.byteLength; | |
| fio._ab = null; | |
| fio._block_pos = 0; | |
| if (fio._file && (fio._pos < fio._size)) { | |
| GFIO._load_block(fio); | |
| } | |
| if (fio._fetch) fio._fetch_wait = true; | |
| } else { | |
| fio._block_pos += bytes; | |
| } | |
| return bytes; | |
| } | |
| //Emscripten's FS | |
| if (!fio._stream) return 0; | |
| if (bytes + fio._pos > fio._size) { | |
| bytes = fio._size - fio._pos; | |
| } | |
| fio._pos += bytes; | |
| let ab = new Uint8Array(LIBGPAC.HEAPU8.buffer, buffer, bytes); | |
| FS.read(fio._stream, ab, 0, bytes); | |
| return bytes; | |
| }, | |
| //write callback from libgpac | |
| write: function(fileio, buffer, bytes) { | |
| let fio = GFIO.to_obj(fileio); | |
| //IO being created, wait | |
| if (fio._select) return 0; | |
| //defer write, cat arrays | |
| if (fio._defer_write) { | |
| let ab = new Uint8Array(LIBGPAC.HEAPU8.buffer, buffer, bytes); | |
| let new_st = new Uint8Array(bytes + fio._defer_write.length); | |
| new_st.set(fio._defer_write); | |
| new_st.set(ab, fio._defer_write.length); | |
| fio._defer_write = new_st; | |
| return bytes; | |
| } | |
| //writable stream API | |
| if (fio._writer) { | |
| let ab = new Uint8Array(LIBGPAC.HEAPU8.buffer, buffer, bytes); | |
| fio._writer.write( new Uint8Array(ab) ); | |
| return bytes; | |
| } | |
| //fetch+PUT+readableStream API | |
| if (fio._upstream) { | |
| let ab = new Uint8Array(LIBGPAC.HEAPU8.buffer, buffer, bytes); | |
| fio._upstream_dst.enqueue(ab); | |
| return bytes; | |
| } | |
| //EM's FS | |
| if (!fio._stream) return 0; | |
| fio._pos+=bytes; | |
| if (fio._size < fio._pos) fio._size = fio._pos; | |
| let ab = new Uint8Array(LIBGPAC.HEAPU8.buffer, buffer, bytes); | |
| FS.write(fio._stream, ab, 0, bytes); | |
| return bytes; | |
| }, | |
| //tell callback from libgpac - we maintain position in resource ourself, so tell/eof are valid for all methods | |
| tell: function(fileio) { | |
| let fio = GFIO.to_obj(fileio); | |
| if (fio._select) return BigInt(0); | |
| return BigInt(fio._pos); | |
| }, | |
| //eof callback from libgpac | |
| eof: function(fileio) { | |
| let fio = GFIO.to_obj(fileio); | |
| if (fio._select==2) return 1; | |
| if (fio._select) return 0; | |
| if (fio._fetch && !fio._size) return 0; | |
| if (fio._pos == fio._size) return 1; | |
| return 0; | |
| }, | |
| //get fileIO object from its userdata - for now we just use an index in the global dict that we reset upon gpac.on_done | |
| to_obj: function(fileio) { | |
| let udta = this.get_udta(fileio); | |
| return this.dict[udta]; | |
| }, | |
| //create file IO from URL and mode (0: r/w, 1: r, 2: w), used by command line parser | |
| create: function (url, mode) { | |
| if (typeof this.wrap_new == 'undefined') { | |
| //gf_fileio_new(url, udta, open, seek, read, write, tell, eof, printf) | |
| this.wrap_new = LIBGPAC.cwrap('gf_fileio_new', 'number', ['string', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number']); | |
| this.wrap_del = LIBGPAC.cwrap('gf_fileio_del', '', ['number']); | |
| this.get_udta = LIBGPAC.cwrap('gf_fileio_get_udta', 'number', ['number']); | |
| this.get_url = LIBGPAC.cwrap('gf_fileio_url', 'string', ['number']); | |
| this.tag_main = LIBGPAC.cwrap('gf_fileio_tag_main_thread', 'number', ['number']); | |
| this.set_stats = LIBGPAC.cwrap('gf_fileio_set_stats', null, ['number', 'bigint', 'bigint', 'boolean', 'number']); | |
| this.set_ready = LIBGPAC.cwrap('gf_fileio_set_write_state', null, ['number', 'number']); | |
| //create function pointers | |
| // | |
| this._pf_open = LIBGPAC.addFunction(this.open, 'iiiii'); | |
| this._pf_seek = LIBGPAC.addFunction(this.seek, 'iiji'); | |
| this._pf_read = LIBGPAC.addFunction(this.read, 'iiii'); | |
| this._pf_write = LIBGPAC.addFunction(this.write, 'iiii'); | |
| this._pf_tell = LIBGPAC.addFunction(this.tell, 'ji'); | |
| this._pf_eof = LIBGPAC.addFunction(this.eof, 'ii'); | |
| } | |
| let wrap_io = false; | |
| if (url.startsWith('$(')) { | |
| let select_end = url.indexOf(')'); | |
| url = url.substring(2, select_end); | |
| wrap_io = true; | |
| } | |
| let fio = { | |
| udta: this.dict.length, | |
| url: url, | |
| _wrap_io: wrap_io, | |
| _pos: 0, | |
| _size: 0, | |
| _stream: null, //FS.open stream object | |
| _file: null, //fileSelector file object | |
| _refs: 0, | |
| _select: 0, | |
| _block_pos_in_file: 0, | |
| _block_pos: 0, //amount of bytes read from block | |
| _ab: null, | |
| _max_bsize: GPAC.read_block_size, //min size, will grow to block_size of fin filter | |
| _discard_next: null, | |
| _timer: null, | |
| _writer: null, | |
| _fetch: null, | |
| _fetch_wait: null, | |
| gfio_url: function() { | |
| return GFIO.get_url(fio._gfio); | |
| } | |
| }; | |
| this.dict.push(fio); | |
| fio._gfio = this.wrap_new(url, fio.udta, this._pf_open, this._pf_seek, (mode==2) ? 0 : this._pf_read, (mode==1) ? 0 : this._pf_write, this._pf_tell, this._pf_eof, 0); | |
| //only run gfio on main thread ? | |
| this.tag_main(fio._gfio); | |
| return fio; | |
| }, | |
| //for file system read, loads next block from file | |
| _load_block: function(fio) { | |
| let max_bsize = fio._max_bsize; | |
| let bsize = (fio._block_pos_in_file + max_bsize > fio._size) ? (fio._size - fio._block_pos_in_file) : max_bsize; | |
| let blob = fio._file.slice(fio._block_pos_in_file, fio._block_pos_in_file + bsize); | |
| let read = new FileReader(); | |
| read.onload = function(evt) { | |
| fio._ab = evt.target.result; | |
| }; | |
| read.readAsArrayBuffer(blob); | |
| } | |
| }; | |
| </script> | |
| <!-- emscriptem module load --> | |
| <script type='text/javascript'> | |
| function prop_to_js(_p, p4cc) { | |
| //TODO: for now we only dump the property to string, we don't map it to JS values | |
| let res = LIBGPAC.props_dump_alloc(p4cc, _p, 0); | |
| if (!res) return null; | |
| let val = LIBGPAC.UTF8ToString(res); | |
| LIBGPAC._free(res); | |
| return val; | |
| } | |
| function load_libgpac(_lib, _lib) | |
| { | |
| let gpac_has_threads = LIBGPAC.cwrap('gpac_has_threads', 'number', []); | |
| _lib.has_threads = gpac_has_threads(); | |
| _lib.in_logs = false; | |
| //error codes | |
| _lib.SCRIPT_INFO = 3; | |
| _lib.PENDING_PACKET = 2; | |
| _lib.EOS = 1; | |
| _lib.OK = 0; | |
| _lib.BAD_PARAM = -1; | |
| _lib.OUT_OF_MEM = -2; | |
| _lib.IO_ERR = -3; | |
| _lib.NOT_SUPPORTED = -4; | |
| _lib.CORRUPTED_DATA = -5; | |
| _lib.SCRIPT_ERROR = -8; | |
| _lib.BUFFER_TOO_SMALL = -9; | |
| _lib.NON_COMPLIANT_BITSTREAM = -10; | |
| _lib.FILTER_NOT_FOUND = -11; | |
| _lib.URL_ERROR = -12; | |
| _lib.SERVICE_ERROR = -13; | |
| _lib.REMOTE_SERVICE_ERROR = -14; | |
| _lib.STREAM_NOT_FOUND = -15; | |
| _lib.URL_REMOVED = -16; | |
| _lib.IP_ADDRESS_NOT_FOUND = -40; | |
| _lib.IP_CONNECTION_FAILURE = -41; | |
| _lib.IP_NETWORK_FAILURE = -42; | |
| _lib.IP_CONNECTION_CLOSED = -43; | |
| _lib.IP_NETWORK_EMPTY = -44; | |
| _lib.IP_UDP_TIMEOUT = -46; | |
| _lib.AUTHENTICATION_FAILURE = -50; | |
| _lib.NOT_READY = -51; | |
| _lib.INVALID_CONFIGURATION = -52; | |
| _lib.NOT_FOUND = -53; | |
| _lib.PROFILE_NOT_SUPPORTED = -54; | |
| _lib.REQUIRES_NEW_INSTANCE = -56; | |
| _lib.FILTER_NOT_SUPPORTED = -57; | |
| //functions | |
| _lib.init = LIBGPAC.cwrap('gf_sys_init', 'number', ['number', 'string']); | |
| _lib.uninit = LIBGPAC.cwrap('gf_sys_close', null, []); | |
| _lib.version = LIBGPAC.cwrap('gf_gpac_version', 'string', []); | |
| _lib.copyright = LIBGPAC.cwrap('gf_gpac_copyright', 'string', []); | |
| _lib.url_cat = LIBGPAC.cwrap('gf_url_concatenate', 'number', ['string', 'string']); | |
| _lib.free = LIBGPAC.cwrap('gf_url_free', null, ['number']); | |
| _lib.log_set_tools_levels = LIBGPAC.cwrap('gf_log_set_tools_levels', 'number', ['string', 'number']); | |
| _lib.e2s = LIBGPAC.cwrap('gf_error_to_string', 'string', ['number']); | |
| //load basic filter session api | |
| _lib.fs_new_defaults = LIBGPAC.cwrap('gf_fs_new_defaults', 'number', ['number']); | |
| _lib.fs_del = LIBGPAC.cwrap('gf_fs_del', null, ['number']); | |
| _lib.fs_print_stats = LIBGPAC.cwrap('gf_fs_print_stats', null, ['number']); | |
| _lib.fs_print_connections = LIBGPAC.cwrap('gf_fs_print_connections', null, ['number']); | |
| _lib.fs_load_filter = LIBGPAC.cwrap('gf_fs_load_filter', 'number', ['number', 'string', 'number']); | |
| _lib.fs_load_source = LIBGPAC.cwrap('gf_fs_load_source', 'number', ['number', 'string', 'string', 'string', 'number']); | |
| _lib.fs_load_destination = LIBGPAC.cwrap('gf_fs_load_destination', 'number', ['number', 'string', 'string', 'string', 'number']); | |
| _lib.fs_run = LIBGPAC.cwrap('gf_fs_run', 'number', ['number']); | |
| _lib.fs_last_task = LIBGPAC.cwrap('gf_fs_is_last_task', 'number', ['number']); | |
| _lib.filter_get_name = LIBGPAC.cwrap('gf_filter_get_name', 'string', ['number']); | |
| _lib.filter_reconnect_output = LIBGPAC.cwrap('gf_filter_reconnect_output', 'number', ['number','number']); | |
| _lib.filter_probe_link = LIBGPAC.cwrap('gf_filter_probe_link', 'number', ['number','number','string', 'number']); | |
| _lib.filter_get_destinations = LIBGPAC.cwrap('gf_filter_get_possible_destinations', 'number', ['number','number', 'number']); | |
| _lib.filter_get_ipid_count = LIBGPAC.cwrap('gf_filter_get_ipid_count', 'number', ['number']); | |
| _lib.filter_get_ipid = LIBGPAC.cwrap('gf_filter_get_ipid', 'number', ['number','number']); | |
| _lib.filter_get_opid_count = LIBGPAC.cwrap('gf_filter_get_opid_count', 'number', ['number']); | |
| _lib.filter_get_opid = LIBGPAC.cwrap('gf_filter_get_opid', 'number', ['number','number']); | |
| _lib.filter_is_sink = LIBGPAC.cwrap('gf_filter_is_sink', 'number', ['number']); | |
| _lib.filter_is_source = LIBGPAC.cwrap('gf_filter_is_source', 'number', ['number']); | |
| _lib.filter_get_max_extra_input_pids = LIBGPAC.cwrap('gf_filter_get_max_extra_input_pids', 'number', ['number']); | |
| _lib.filter_pid_get_property = LIBGPAC.cwrap('gf_filter_pid_get_property', 'number', ['number', 'number']); | |
| _lib.filter_pid_get_property_str = LIBGPAC.cwrap('gf_filter_pid_get_property', 'number', ['number', 'string']); | |
| _lib.filter_pid_enum_properties = LIBGPAC.cwrap('gf_filter_pid_enum_properties', 'number', ['number', 'number', 'number', 'string']); | |
| _lib.props_4cc_name = LIBGPAC.cwrap('gf_props_4cc_get_name', 'string', ['number']); | |
| _lib.props_dump_alloc = LIBGPAC.cwrap('gf_props_dump_alloc', 'number', ['number','number','number']); | |
| _lib.filter_pid_get_props = (pid) => { | |
| props = []; | |
| const p_idx = LIBGPAC._malloc(LIBGPAC.HEAP32.BYTES_PER_ELEMENT); | |
| const p_4cc = LIBGPAC._malloc(LIBGPAC.HEAP32.BYTES_PER_ELEMENT); | |
| const p_name = LIBGPAC._malloc(LIBGPAC.HEAP32.BYTES_PER_ELEMENT); | |
| LIBGPAC.HEAP32.set([0], p_idx/LIBGPAC.HEAP32.BYTES_PER_ELEMENT); | |
| while (1) { | |
| let _p = _lib.filter_pid_enum_properties(pid, p_idx, p_4cc, p_name); | |
| if (!_p) break; | |
| let prop_4cc = LIBGPAC.getValue(p_4cc, 'i32'); | |
| let c_str = LIBGPAC.getValue(p_name, "i32"); //const, no need to free | |
| let prop_name = c_str ? LIBGPAC.UTF8ToString(c_str) : null; | |
| p = {}; | |
| p.name = prop_4cc ? _lib.props_4cc_name(prop_4cc) : prop_name; | |
| p.value = prop_to_js(_p, prop_4cc); | |
| if (p.value) | |
| props.push(p); | |
| } | |
| LIBGPAC._free(p_idx); | |
| LIBGPAC._free(p_4cc); | |
| LIBGPAC._free(p_name); | |
| return props; | |
| }; | |
| } | |
| var GPAC = { | |
| on_done: function() { | |
| LIBGPAC.gpac_set_console(3); | |
| set_status(''); | |
| GPAC.running = false; | |
| GPAC.do_log = false; | |
| //in case we had an exception, force a gf_sys_close | |
| LIBGPAC.uninit(); | |
| if (GPAC.restore_worker) { | |
| LIBGPAC.gpac_worker = true; | |
| GPAC.restore_worker = false; | |
| } | |
| GFIO.dict.forEach(fio => { | |
| if (fio._timer) { | |
| window.clearTimeout(fio._timer); | |
| fio._timer=null; | |
| } | |
| }); | |
| GFIO.dict = []; | |
| animElement.stop(); | |
| cmdElement.readonly = false; | |
| //restore emscripten stack | |
| if (GPAC.stack != null) { | |
| var stack_init = GPAC.stack; | |
| GPAC.stack = null; | |
| function restore_stack() { | |
| LIBGPAC.stackRestore(stack_init); | |
| } | |
| //we may be called asynchronously here if emscripten main loop is used, so restore stack on main thread only | |
| setTimeout(restore_stack, 1); | |
| } | |
| }, | |
| running: false, | |
| no_log: true, | |
| stack: null, | |
| anim: animElement, | |
| restore_worker: false, | |
| read_block_size: 10000 | |
| }; | |
| function displayStatus(text, busy = false, progress = null){ | |
| statusElement.innerHTML = text; | |
| progressTxt.nodeValue = progress !== null ? `${Math.floor(val)} %` : ''; | |
| if (busy){ | |
| animElement.start(); | |
| } else { | |
| animElement.stop(); | |
| } | |
| } | |
| function check_threading() { | |
| const threaded = LIBGPAC.has_threads; | |
| const threadedOnly = [...document.getElementsByClassName('threaded')]; | |
| threadedOnly.forEach(el => el.hidden = !threaded); | |
| const thStatus = document.getElementById('threading_status'); | |
| threadingElement.checked = threaded; | |
| if (window.location.href.indexOf('.gpac.io')<0) { | |
| threadingElement.disabled = true; | |
| } else { | |
| threadingElement.onchange = () => { | |
| const hostname = threaded ? 'wasm.gpac.io' : 'wasmth.gpac.io'; | |
| window.location.href = `https://${hostname}/`; | |
| console.log('Reloading to ' + window.location.href); | |
| } | |
| } | |
| } | |
| const LIBGPAC = { | |
| preRun: [ | |
| // function() { LIBGPAC.ENV.SDL_EMSCRIPTEN_KEYBOARD_ELEMENT = "#canvas"; } | |
| ], | |
| postRun: [ | |
| ], | |
| print: (function() { | |
| if (outputElement) outputElement.innerHTML = ''; // clear browser cache | |
| return function(text) { | |
| if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); | |
| // These replacements are necessary if you render to raw HTML | |
| text = text.replace(/&/g, "&"); | |
| text = text.replace(/</g, "<"); | |
| text = text.replace(/>/g, ">"); | |
| text = text.replace(/\n/g, '<br>', 'g'); | |
| text = text.replace(/\t/g, ' '); | |
| // handle \r | |
| var prevPos = 0; | |
| var output = ''; | |
| for (var i = 0; i < text.length; i++) { | |
| if (text.charCodeAt(i) == 13) { | |
| output += text.substring(prevPos, i); | |
| prevPos = i + 1; | |
| } | |
| } | |
| // convert ANSI colors to HTML | |
| text = ansiHTML(text); | |
| if (GPAC.no_log) | |
| console.log(text); | |
| else if (outputElement) { | |
| outputElement.innerHTML += text + "<br>"; | |
| outputElement.scrollTop = outputElement.scrollHeight; // focus on bottom | |
| } | |
| }; | |
| })(), | |
| printErr: function(text) { | |
| if (LIBGPAC.in_logs) | |
| console.log(text); | |
| else | |
| LIBGPAC.print(text); | |
| }, | |
| canvas: (function() { | |
| var canvas = document.getElementById('canvas'); | |
| // As a default initial behavior, pop up an alert when webgl context is lost. To make your | |
| // application robust, you may want to override this behavior before shipping! | |
| // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2 | |
| canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false); | |
| return canvas; | |
| })(), | |
| setStatus: function(text) { | |
| if (!LIBGPAC.setStatus.last) LIBGPAC.setStatus.last = { time: Date.now(), text: '' }; | |
| if (text === LIBGPAC.setStatus.last.text) return; | |
| var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/); | |
| var now = Date.now(); | |
| if (m && now - LIBGPAC.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon | |
| LIBGPAC.setStatus.last.time = now; | |
| LIBGPAC.setStatus.last.text = text; | |
| const busy = !!m; | |
| let progress = null; | |
| if (busy) { | |
| text = m[1]; | |
| progress = parseInt(m[2])*100 / parseInt(m[4]); | |
| } | |
| displayStatus(text, busy, progress); | |
| }, | |
| totalDependencies: 0, | |
| monitorRunDependencies: function(left) { | |
| this.totalDependencies = Math.max(this.totalDependencies, left); | |
| LIBGPAC.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.'); | |
| }, | |
| onRuntimeInitialized: function() { | |
| FS = LIBGPAC['FS']; | |
| load_libgpac(LIBGPAC, LIBGPAC); | |
| GPAC.toggle_reporting = function(value) { | |
| const gpac_em_sig_handler = LIBGPAC.cwrap('gpac_em_sig_handler', null, ['number']); | |
| gpac_em_sig_handler(value); | |
| }; | |
| document.addEventListener("keydown", (event) => { | |
| if (event.ctrlKey) { | |
| switch (event.keyCode) { | |
| case 76: // Ctrl+L | |
| case 65: // Ctrl+A | |
| case 69: // Ctrl+E | |
| event.preventDefault(); | |
| break; | |
| } | |
| } | |
| }) | |
| //mount IDBFS | |
| FS.mkdir('/idbfs'); | |
| FS.mount(LIBGPAC['IDBFS'], {}, '/idbfs'); | |
| FS.syncfs(true, () => { | |
| if (arguments.length) { | |
| err('Failed to sync IDBFS: ' + arguments[0] + ' - will use Emscripten $HOME for config'); | |
| } else { | |
| load_history(); | |
| } | |
| }); | |
| check_threading(); | |
| onGpacReady(); | |
| }, | |
| //do not run until we have something to run | |
| noInitialRun: true, | |
| //do not exit runtime on exit() | |
| noExitRuntime: true, | |
| //GPAC-specific extensions of the module object, only used by gpac_main.c (NOT USED by libgpac functions) | |
| //callback function called by gpac_main when exiting | |
| gpac_done: function(code) { | |
| if (code) do_log('(exit code ' + code +')'); | |
| GPAC.on_done(); | |
| com_exit(); | |
| }, | |
| //set console function called by gpac_main | |
| //code is 1 for console clear, 2 for console save and 3 for console restore (color codes are not forwarded) | |
| gpac_set_console: function(code) { | |
| if (!outputElement) return; | |
| switch (code) { | |
| //clear | |
| case 1: | |
| outputElement.value = ''; | |
| return; | |
| //save | |
| case 2: | |
| if (console_bck.length) return; | |
| console_bck = outputElement.value; | |
| outputElement.value = ''; | |
| return; | |
| //restore | |
| case 3: | |
| if (!console_bck) return; | |
| outputElement.value = console_bck; | |
| console_bck = ''; | |
| return; | |
| } | |
| }, | |
| //worker flag used by gpac_main to launch as worker | |
| gpac_worker: false, | |
| //boolean flag disabling/enabling fetch() - if undefined or not false, fetch() is enabled | |
| gpac_fetch: true, | |
| //fetch mode or cors | |
| gpac_fetch_mode: 'cors', | |
| //extra headers to add to fetch requests | |
| gpac_extra_headers: {} | |
| }; | |
| displayStatus('Downloading...', true); | |
| window.onerror = function(event) { | |
| // TODO: do not warn on ok events like simulating an infinite loop or exitStatus | |
| displayStatus('Exception thrown, see JavaScript console'); | |
| if (LIBGPAC){ | |
| LIBGPAC.setStatus = function(text) { | |
| if (text) console.error('[post-exception status] ' + text); | |
| }; | |
| } | |
| }; | |
| </script> | |
| <!-- load module - default emscripten compilation is with MODULARIZE, EXPORT_ES6 and export name "libgpac" --> | |
| <script type="module" async> | |
| import libgpac from './gpac.js'; | |
| window.addEventListener('load', function() { | |
| libgpac(LIBGPAC); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |