From def0bb36d679b94208bdda16452a728608fb46af Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 19 Jul 2026 09:14:57 +0300 Subject: [PATCH 1/5] Probe JavaScript video encoder capabilities --- .../codename1/impl/html5/HTML5VideoIO.java | 42 +++++++++++++++++-- Ports/JavaScriptPort/src/main/webapp/port.js | 18 ++++++++ .../src/javascript/browser_bridge.js | 36 ++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java index b718e57a4a..35de935ce8 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java @@ -62,10 +62,16 @@ public VideoCodec[] getAvailableEncoders() { return new VideoCodec[0]; } List out = new ArrayList(); - out.add(new VideoCodec(CODEC_H264, "H.264 (WebCodecs)", "video/avc", true, true, false, true, -1, -1, new String[]{CONTAINER_MP4})); - out.add(new VideoCodec(CODEC_VP9, "VP9 (WebCodecs)", "video/vp9", true, true, false, true, -1, -1, new String[]{CONTAINER_WEBM})); - if (cn1AudioWebCodecsAvailable()) { + if (cn1VideoEncoderSupported("avc1.42001f")) { + out.add(new VideoCodec(CODEC_H264, "H.264 (WebCodecs)", "video/avc", true, true, false, true, -1, -1, new String[]{CONTAINER_MP4})); + } + if (cn1VideoEncoderSupported("vp09.00.10.08")) { + out.add(new VideoCodec(CODEC_VP9, "VP9 (WebCodecs)", "video/vp9", true, true, false, true, -1, -1, new String[]{CONTAINER_WEBM})); + } + if (cn1AudioWebCodecsAvailable() && cn1AudioEncoderSupported("mp4a.40.2")) { out.add(new VideoCodec(CODEC_AAC, "AAC (WebCodecs)", "audio/mp4a-latm", false, true, false, false, -1, -1, new String[]{CONTAINER_MP4})); + } + if (cn1AudioWebCodecsAvailable() && cn1AudioEncoderSupported("opus")) { out.add(new VideoCodec(CODEC_OPUS, "Opus (WebCodecs)", "audio/opus", false, true, false, false, -1, -1, new String[]{CONTAINER_WEBM})); } return out.toArray(new VideoCodec[out.size()]); @@ -341,6 +347,36 @@ private static String safeError(int peer) { @JSBody(params = {}, script = "return (typeof AudioEncoder !== 'undefined' && typeof AudioData !== 'undefined');") private static native boolean cn1AudioWebCodecsAvailable(); + @JSBody(params = {"codec"}, script = + "if (typeof VideoEncoder === 'undefined') { return false; }" + + "var encoder = null;" + + "try {" + + " encoder = new VideoEncoder({ output: function() {}, error: function() {} });" + + " var config = { codec: codec, width: 128, height: 96, bitrate: 800000, framerate: 6 };" + + " if (codec.indexOf('avc1.') === 0) { config.avc = { format: 'avc' }; }" + + " encoder.configure(config);" + + " encoder.close();" + + " return true;" + + "} catch (e) {" + + " try { if (encoder && encoder.state !== 'closed') { encoder.close(); } } catch (ignored) {}" + + " return false;" + + "}") + private static native boolean cn1VideoEncoderSupported(String codec); + + @JSBody(params = {"codec"}, script = + "if (typeof AudioEncoder === 'undefined') { return false; }" + + "var encoder = null;" + + "try {" + + " encoder = new AudioEncoder({ output: function() {}, error: function() {} });" + + " encoder.configure({ codec: codec, sampleRate: 48000, numberOfChannels: 1, bitrate: 128000 });" + + " encoder.close();" + + " return true;" + + "} catch (e) {" + + " try { if (encoder && encoder.state !== 'closed') { encoder.close(); } } catch (ignored) {}" + + " return false;" + + "}") + private static native boolean cn1AudioEncoderSupported(String codec); + // ============================================================= JS: decoder @JSBody(params = {"url"}, script = diff --git a/Ports/JavaScriptPort/src/main/webapp/port.js b/Ports/JavaScriptPort/src/main/webapp/port.js index 0b37c4c844..ef185565e7 100644 --- a/Ports/JavaScriptPort/src/main/webapp/port.js +++ b/Ports/JavaScriptPort/src/main/webapp/port.js @@ -2328,6 +2328,24 @@ bindNative([ return (yield* cn1VideoIoHost({ op: "audioWebCodecsAvailable" })) ? 1 : 0; }); +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoEncoderSupported_java_lang_String_R_boolean", + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoEncoderSupported___java_lang_String_R_boolean" +], function*(codec) { + return (yield* cn1VideoIoHost({ + op: "videoEncoderSupported", codec: jvm.toNativeString(codec) + })) ? 1 : 0; +}); + +bindNative([ + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1AudioEncoderSupported_java_lang_String_R_boolean", + "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1AudioEncoderSupported___java_lang_String_R_boolean" +], function*(codec) { + return (yield* cn1VideoIoHost({ + op: "audioEncoderSupported", codec: jvm.toNativeString(codec) + })) ? 1 : 0; +}); + bindNative([ "cn1_com_codename1_impl_html5_HTML5VideoIO_cn1VideoOpen_java_lang_String_R_int" ], function*(url) { diff --git a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js index 84e4afcea2..346bd1e595 100644 --- a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js +++ b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js @@ -2805,6 +2805,42 @@ if (op === 'audioWebCodecsAvailable') { return !!(global.AudioEncoder && global.AudioData); } + if (op === 'videoEncoderSupported') { + if (!global.VideoEncoder || typeof global.VideoEncoder.isConfigSupported !== 'function') { + return false; + } + var videoCodec = String(payload.codec || ''); + var videoSupportConfig = { + codec: videoCodec, + width: 128, + height: 96, + bitrate: 800000, + framerate: 6 + }; + if (videoCodec.indexOf('avc1.') === 0) { + videoSupportConfig.avc = { format: 'avc' }; + } + return global.VideoEncoder.isConfigSupported(videoSupportConfig).then(function(result) { + return !!(result && result.supported); + }, function() { + return false; + }); + } + if (op === 'audioEncoderSupported') { + if (!global.AudioEncoder || typeof global.AudioEncoder.isConfigSupported !== 'function') { + return false; + } + return global.AudioEncoder.isConfigSupported({ + codec: String(payload.codec || ''), + sampleRate: 48000, + numberOfChannels: 1, + bitrate: 128000 + }).then(function(result) { + return !!(result && result.supported); + }, function() { + return false; + }); + } if (op === 'videoBlobUrl') { var blobBytes = cn1VideoIoBase64Bytes(payload.b64); var blob = new global.Blob([blobBytes], { From 3809e32fa578ecc10a2f6fe1c02a07351569cdc0 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 19 Jul 2026 10:45:27 +0300 Subject: [PATCH 2/5] Probe actual WebCodecs encoder output --- .../codename1/impl/html5/HTML5VideoIO.java | 11 +- .../src/javascript/browser_bridge.js | 150 ++++++++++++++---- 2 files changed, 127 insertions(+), 34 deletions(-) diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java index 35de935ce8..f201ec43c8 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java @@ -49,7 +49,7 @@ /// which lets the browser's asynchronous `seeked`/`loadedmetadata` events fire while a /// decode call appears synchronous to the caller. /// -/// **Encoding** uses the WebCodecs `VideoEncoder`/`AudioEncoder` (H.264/VP9 + AAC/Opus) +/// **Encoding** uses the WebCodecs `VideoEncoder`/`AudioEncoder` (H.264/VP8/VP9 + AAC/Opus) /// muxed into MP4/WebM with the standalone `mp4-muxer`/`webm-muxer` libraries (loaded /// from a CDN, the same mechanism the port uses for VideoJS). Frames are pushed as RGBA, /// the muxer's in-memory buffer is handed back to Java on `close()` and written to the @@ -65,6 +65,9 @@ public VideoCodec[] getAvailableEncoders() { if (cn1VideoEncoderSupported("avc1.42001f")) { out.add(new VideoCodec(CODEC_H264, "H.264 (WebCodecs)", "video/avc", true, true, false, true, -1, -1, new String[]{CONTAINER_MP4})); } + if (cn1VideoEncoderSupported("vp8")) { + out.add(new VideoCodec(CODEC_VP8, "VP8 (WebCodecs)", "video/vp8", true, true, false, true, -1, -1, new String[]{CONTAINER_WEBM})); + } if (cn1VideoEncoderSupported("vp09.00.10.08")) { out.add(new VideoCodec(CODEC_VP9, "VP9 (WebCodecs)", "video/vp9", true, true, false, true, -1, -1, new String[]{CONTAINER_WEBM})); } @@ -229,8 +232,12 @@ static final class Writer extends VideoWriter { if (br <= 0) { br = (int) Math.max(800000L, Math.min((long) (width * (long) height * Math.max(1f, frameRate) * 0.10), 100000000L)); } + int audioBr = cfg.getAudioBitRate(); + if (hasAudio && audioBr <= 0) { + audioBr = 128000; + } this.peer = cn1EncOpen(cfg.getContainer(), cfg.getVideoCodec(), width, height, Math.round(frameRate), br, - hasAudio, cfg.getAudioCodec(), cfg.getAudioBitRate(), cfg.getSampleRate(), cfg.getAudioChannels()); + hasAudio, cfg.getAudioCodec(), audioBr, cfg.getSampleRate(), cfg.getAudioChannels()); if (peer == 0) { throw new IOException("Failed to configure the WebCodecs encoder: " + safeError(0)); } diff --git a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js index 346bd1e595..6444ee8aa2 100644 --- a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js +++ b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js @@ -2794,6 +2794,122 @@ return output; } + // isConfigSupported() only validates the shape of a WebCodecs configuration + // in some Chromium builds. Linux headless Chromium can report H.264 as + // supported there and then deliver NotSupportedError through the encoder's + // asynchronous error callback. Probe the operation we actually depend on: + // configure, encode one frame, and flush it successfully. + function cn1VideoIoProbeVideoEncoder(codec) { + if (!global.VideoEncoder || !global.VideoFrame) { + return Promise.resolve(false); + } + return new Promise(function(resolve) { + var encoder = null; + var frame = null; + var timer = null; + var settled = false; + var producedOutput = false; + var finish = function(supported) { + if (settled) { return; } + settled = true; + if (timer !== null) { global.clearTimeout(timer); } + try { if (frame) { frame.close(); } } catch (_frameCloseError) {} + try { + if (encoder && encoder.state !== 'closed') { encoder.close(); } + } catch (_encoderCloseError) {} + resolve(!!supported); + }; + timer = global.setTimeout(function() { finish(false); }, 5000); + try { + encoder = new global.VideoEncoder({ + output: function() { producedOutput = true; }, + error: function() { finish(false); } + }); + var config = { + codec: String(codec || ''), + width: 128, + height: 96, + bitrate: 800000, + framerate: 6 + }; + if (config.codec.indexOf('avc1.') === 0) { + config.avc = { format: 'avc' }; + } + encoder.configure(config); + frame = new global.VideoFrame(new Uint8Array(128 * 96 * 4), { + format: 'RGBA', + codedWidth: 128, + codedHeight: 96, + timestamp: 0 + }); + encoder.encode(frame, { keyFrame: true }); + frame.close(); + frame = null; + encoder.flush().then(function() { + finish(producedOutput); + }, function() { + finish(false); + }); + } catch (_probeError) { + finish(false); + } + }); + } + + function cn1VideoIoProbeAudioEncoder(codec) { + if (!global.AudioEncoder || !global.AudioData) { + return Promise.resolve(false); + } + return new Promise(function(resolve) { + var encoder = null; + var audio = null; + var timer = null; + var settled = false; + var producedOutput = false; + var finish = function(supported) { + if (settled) { return; } + settled = true; + if (timer !== null) { global.clearTimeout(timer); } + try { if (audio) { audio.close(); } } catch (_audioCloseError) {} + try { + if (encoder && encoder.state !== 'closed') { encoder.close(); } + } catch (_audioEncoderCloseError) {} + resolve(!!supported); + }; + timer = global.setTimeout(function() { finish(false); }, 5000); + try { + encoder = new global.AudioEncoder({ + output: function() { producedOutput = true; }, + error: function() { finish(false); } + }); + encoder.configure({ + codec: String(codec || ''), + sampleRate: 48000, + numberOfChannels: 1, + bitrate: 128000 + }); + audio = new global.AudioData({ + format: 's16', + sampleRate: 48000, + numberOfFrames: 4800, + numberOfChannels: 1, + timestamp: 0, + data: new Int16Array(4800) + }); + encoder.encode(audio); + audio.close(); + audio = null; + encoder.flush().then(function() { + finish(producedOutput); + }, function() { + finish(false); + }); + } catch (_probeError) { + finish(false); + } + }); + } + hostBridge.register('__cn1_video_io__', function(request) { var payload = request || {}; var op = String(payload.op || ''); @@ -2806,40 +2922,10 @@ return !!(global.AudioEncoder && global.AudioData); } if (op === 'videoEncoderSupported') { - if (!global.VideoEncoder || typeof global.VideoEncoder.isConfigSupported !== 'function') { - return false; - } - var videoCodec = String(payload.codec || ''); - var videoSupportConfig = { - codec: videoCodec, - width: 128, - height: 96, - bitrate: 800000, - framerate: 6 - }; - if (videoCodec.indexOf('avc1.') === 0) { - videoSupportConfig.avc = { format: 'avc' }; - } - return global.VideoEncoder.isConfigSupported(videoSupportConfig).then(function(result) { - return !!(result && result.supported); - }, function() { - return false; - }); + return cn1VideoIoProbeVideoEncoder(payload.codec); } if (op === 'audioEncoderSupported') { - if (!global.AudioEncoder || typeof global.AudioEncoder.isConfigSupported !== 'function') { - return false; - } - return global.AudioEncoder.isConfigSupported({ - codec: String(payload.codec || ''), - sampleRate: 48000, - numberOfChannels: 1, - bitrate: 128000 - }).then(function(result) { - return !!(result && result.supported); - }, function() { - return false; - }); + return cn1VideoIoProbeAudioEncoder(payload.codec); } if (op === 'videoBlobUrl') { var blobBytes = cn1VideoIoBase64Bytes(payload.b64); From cd53d481351b4caaceb0bd75c064983e804de0ec Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:28:54 +0300 Subject: [PATCH 3/5] Reuse JavaScript video encoder capability probes --- .../src/javascript/browser_bridge.js | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js index 6444ee8aa2..67faf77e50 100644 --- a/vm/ByteCodeTranslator/src/javascript/browser_bridge.js +++ b/vm/ByteCodeTranslator/src/javascript/browser_bridge.js @@ -2794,6 +2794,24 @@ return output; } + // Capability discovery is intentionally destructive: unlike + // isConfigSupported(), these probes create a real encoder and require it to + // produce output. Cache each Promise for the lifetime of the page so callers + // such as isEncoderSupported() don't repeatedly allocate codec sessions. + // Repeated create/close cycles can temporarily exhaust Chromium's Linux + // software codec factories and make the immediately following real writer + // fail with NotSupportedError even though the probe just succeeded. + var cn1VideoIoVideoEncoderSupport = Object.create(null); + var cn1VideoIoAudioEncoderSupport = Object.create(null); + + function cn1VideoIoCachedEncoderSupport(cache, codec, probe) { + var key = String(codec || ''); + if (!Object.prototype.hasOwnProperty.call(cache, key)) { + cache[key] = probe(key); + } + return cache[key]; + } + // isConfigSupported() only validates the shape of a WebCodecs configuration // in some Chromium builds. Linux headless Chromium can report H.264 as // supported there and then deliver NotSupportedError through the encoder's @@ -2817,7 +2835,10 @@ try { if (encoder && encoder.state !== 'closed') { encoder.close(); } } catch (_encoderCloseError) {} - resolve(!!supported); + // WebCodecs close() initiates teardown of the platform encoder. Give + // Chromium a macrotask to release that session before discovery moves + // on to another codec or the application opens its real writer. + global.setTimeout(function() { resolve(!!supported); }, 0); }; timer = global.setTimeout(function() { finish(false); }, 5000); try { @@ -2874,7 +2895,7 @@ try { if (encoder && encoder.state !== 'closed') { encoder.close(); } } catch (_audioEncoderCloseError) {} - resolve(!!supported); + global.setTimeout(function() { resolve(!!supported); }, 0); }; timer = global.setTimeout(function() { finish(false); }, 5000); try { @@ -2922,10 +2943,12 @@ return !!(global.AudioEncoder && global.AudioData); } if (op === 'videoEncoderSupported') { - return cn1VideoIoProbeVideoEncoder(payload.codec); + return cn1VideoIoCachedEncoderSupport(cn1VideoIoVideoEncoderSupport, + payload.codec, cn1VideoIoProbeVideoEncoder); } if (op === 'audioEncoderSupported') { - return cn1VideoIoProbeAudioEncoder(payload.codec); + return cn1VideoIoCachedEncoderSupport(cn1VideoIoAudioEncoderSupport, + payload.codec, cn1VideoIoProbeAudioEncoder); } if (op === 'videoBlobUrl') { var blobBytes = cn1VideoIoBase64Bytes(payload.b64); @@ -3066,9 +3089,9 @@ encoderState.videoEncoder = new global.VideoEncoder({ output: function(chunk, meta) { try { muxer.addVideoChunk(chunk, meta); } - catch (error) { encoderState.error = String(error); } + catch (error) { encoderState.error = 'video muxer: ' + String(error); } }, - error: function(error) { encoderState.error = String(error); } + error: function(error) { encoderState.error = 'video encoder: ' + String(error); } }); var videoConfig = { codec: videoConfigCodec, @@ -3086,9 +3109,9 @@ encoderState.audioEncoder = new global.AudioEncoder({ output: function(chunk, meta) { try { muxer.addAudioChunk(chunk, meta); } - catch (error) { encoderState.error = String(error); } + catch (error) { encoderState.error = 'audio muxer: ' + String(error); } }, - error: function(error) { encoderState.error = String(error); } + error: function(error) { encoderState.error = 'audio encoder: ' + String(error); } }); encoderState.audioEncoder.configure({ codec: audioConfigCodec, @@ -3124,7 +3147,7 @@ frameEncoder.videoEncoder.encode(frame); frame.close(); } catch (error) { - frameEncoder.error = String(error); + frameEncoder.error = 'video frame: ' + String(error); } return null; } @@ -3147,7 +3170,7 @@ audioEncoder.audioEncoder.encode(audioData); audioData.close(); } catch (error) { - audioEncoder.error = String(error); + audioEncoder.error = 'audio frame: ' + String(error); } return null; } @@ -3165,7 +3188,7 @@ flushEncoder.result = flushEncoder.target.buffer; }) .catch(function(error) { - flushEncoder.error = String(error); + flushEncoder.error = 'encoder flush: ' + String(error); }) .then(function() { flushEncoder.done = true; From 309d8c09959c276908514eff0c5c85b2bb3ee014 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:17:31 +0300 Subject: [PATCH 4/5] Match JavaScript video and audio codecs --- .../codename1/impl/html5/HTML5VideoIO.java | 18 ++++++ .../tests/VideoIORoundTripTest.java | 62 ++++++++++++------- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java index f201ec43c8..f16329eaae 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5VideoIO.java @@ -220,6 +220,24 @@ static final class Writer extends VideoWriter { this.hasAudio = cfg.isHasAudio(); this.outPath = cfg.getPath(); + boolean webm = CONTAINER_WEBM.equals(cfg.getContainer()); + boolean mp4 = CONTAINER_MP4.equals(cfg.getContainer()); + if (!webm && !mp4) { + throw new IOException("Unsupported JavaScript video container: " + cfg.getContainer()); + } + if (webm && !CODEC_VP8.equals(cfg.getVideoCodec()) && !CODEC_VP9.equals(cfg.getVideoCodec())) { + throw new IOException("WebM requires VP8 or VP9 video, not " + cfg.getVideoCodec()); + } + if (mp4 && !CODEC_H264.equals(cfg.getVideoCodec())) { + throw new IOException("MP4 requires H.264 video, not " + cfg.getVideoCodec()); + } + if (hasAudio && webm && !CODEC_OPUS.equals(cfg.getAudioCodec())) { + throw new IOException("WebM requires Opus audio, not " + cfg.getAudioCodec()); + } + if (hasAudio && mp4 && !CODEC_AAC.equals(cfg.getAudioCodec())) { + throw new IOException("MP4 requires AAC audio, not " + cfg.getAudioCodec()); + } + cn1EncEnsureLibs(); long start = System.currentTimeMillis(); while (!cn1EncLibsReady(cfg.getContainer())) { diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/VideoIORoundTripTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/VideoIORoundTripTest.java index 60ca707a97..d9834bc0bf 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/VideoIORoundTripTest.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/VideoIORoundTripTest.java @@ -78,23 +78,48 @@ private void runRoundTrip() { return; } - // Pick an available video codec (prefer H.264) and audio codec (prefer AAC). - String videoCodec = null; - String audioCodec = null; + // Pick a container-compatible pair. Audio and video codecs cannot be + // selected independently: Opus is valid for WebM but not MP4, while + // AAC is valid for MP4 but not WebM. Prefer a complete A/V round trip, + // then fall back to a video-only round trip when the browser has no + // compatible audio encoder. + boolean h264 = false; + boolean vp8 = false; + boolean vp9 = false; + boolean aac = false; + boolean opus = false; for (VideoCodec c : io.getAvailableEncoders()) { - if (c.isVideo()) { - if (VideoIO.CODEC_H264.equals(c.getId())) { - videoCodec = c.getId(); - } else if (videoCodec == null) { - videoCodec = c.getId(); - } - } else { - if (VideoIO.CODEC_AAC.equals(c.getId())) { - audioCodec = c.getId(); - } else if (audioCodec == null) { - audioCodec = c.getId(); - } - } + String id = c.getId(); + h264 |= VideoIO.CODEC_H264.equals(id); + vp8 |= VideoIO.CODEC_VP8.equals(id); + vp9 |= VideoIO.CODEC_VP9.equals(id); + aac |= VideoIO.CODEC_AAC.equals(id); + opus |= VideoIO.CODEC_OPUS.equals(id); + } + + String videoCodec; + String audioCodec; + if (h264 && aac) { + videoCodec = VideoIO.CODEC_H264; + audioCodec = VideoIO.CODEC_AAC; + } else if (vp8 && opus) { + videoCodec = VideoIO.CODEC_VP8; + audioCodec = VideoIO.CODEC_OPUS; + } else if (vp9 && opus) { + videoCodec = VideoIO.CODEC_VP9; + audioCodec = VideoIO.CODEC_OPUS; + } else if (h264) { + videoCodec = VideoIO.CODEC_H264; + audioCodec = null; + } else if (vp8) { + videoCodec = VideoIO.CODEC_VP8; + audioCodec = null; + } else if (vp9) { + videoCodec = VideoIO.CODEC_VP9; + audioCodec = null; + } else { + videoCodec = null; + audioCodec = null; } if (videoCodec == null) { skip("no-video-encoder-on-" + Display.getInstance().getPlatformName()); @@ -105,11 +130,6 @@ private void runRoundTrip() { String container = webm ? VideoIO.CONTAINER_WEBM : VideoIO.CONTAINER_MP4; String mime = webm ? "video/webm" : "video/mp4"; boolean withAudio = audioCodec != null; - if (webm && withAudio && VideoIO.CODEC_AAC.equals(audioCodec)) { - // AAC isn't a WebM audio codec; only keep audio if Opus is available. - withAudio = io.isEncoderSupported(VideoIO.CODEC_OPUS); - audioCodec = VideoIO.CODEC_OPUS; - } String path = FileSystemStorage.getInstance().getAppHomePath() + "/cn1-videoio-roundtrip-" + System.currentTimeMillis() + (webm ? ".webm" : ".mp4"); From 6403053be962395c7c8b889f86a91e2c12f6ef65 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:57:03 +0300 Subject: [PATCH 5/5] Add round-trip test copyright header --- .../tests/VideoIORoundTripTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/VideoIORoundTripTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/VideoIORoundTripTest.java index d9834bc0bf..f4bede2c9d 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/VideoIORoundTripTest.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/VideoIORoundTripTest.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package com.codenameone.examples.hellocodenameone.tests; import com.codename1.io.FileSystemStorage;