diff --git a/package.json b/package.json index 1822e7f..170158d 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "install": "node scripts/install.js", "build": "node-gyp rebuild", "demo": "node demo/demo.js", - "test": "node test/app-lifecycle.js && node test/animation.js && node test/layer-transform.js && node test/contents-gravity.js && node test/contents-center.js && node test/accessibility-display.js && node test/ignores-mouse-events.js && node test/popup-menu.js && node test/pasteboard-write.js && node test/surface-blit.js && node test/surface-faded.js && node test/image-smoothing.js && node test/surface-colorspace.js && node test/video-surface.js && node test/player.js && node test/round-rect-xy.js && node test/clip-rule.js && node test/radial-gradient.js && node test/control-bezels.js && node test/text-features.js && node test/font-from-data.js && node test/text-coverage.js && node test/text-typesetter.js && node test/text-line-height.js && node test/text-line-width.js && node test/text-justify.js && node test/text-scale.js && node test/symbols.js && node test/eventkit-permissions.js && node test/calendars.js && node test/calendar-writes.js && node test/screencolor.js && node test/notifications.js && node test/threaded.js && node test/threaded-verbs.js && node test/threaded-frames.js && node test/threaded-resize.js && node test/ending-worker.js && node test/live-resize.js && node test/window-states.js && node test/threaded-window-states.js && node test/activation-policy.js && node test/last-input.js" + "test": "node test/app-lifecycle.js && node test/animation.js && node test/layer-transform.js && node test/contents-gravity.js && node test/contents-center.js && node test/accessibility-display.js && node test/ignores-mouse-events.js && node test/popup-menu.js && node test/pasteboard-write.js && node test/surface-blit.js && node test/surface-faded.js && node test/image-smoothing.js && node test/surface-colorspace.js && node test/video-surface.js && node test/player.js && node test/player-vp9.js && node test/round-rect-xy.js && node test/clip-rule.js && node test/radial-gradient.js && node test/control-bezels.js && node test/text-features.js && node test/font-from-data.js && node test/text-coverage.js && node test/text-typesetter.js && node test/text-line-height.js && node test/text-line-width.js && node test/text-justify.js && node test/text-scale.js && node test/symbols.js && node test/eventkit-permissions.js && node test/calendars.js && node test/calendar-writes.js && node test/screencolor.js && node test/notifications.js && node test/threaded.js && node test/threaded-verbs.js && node test/threaded-frames.js && node test/threaded-resize.js && node test/ending-worker.js && node test/live-resize.js && node test/window-states.js && node test/threaded-window-states.js && node test/activation-policy.js && node test/last-input.js" }, "dependencies": { "node-addon-api": "^8.3.0" diff --git a/src/player.mm b/src/player.mm index e38c0b8..55a00a9 100644 --- a/src/player.mm +++ b/src/player.mm @@ -31,6 +31,7 @@ #include #import +#import #import #import #import @@ -259,8 +260,21 @@ static void Apply(CALPlayer* p, const PlayerSettings& s) { } } +// VP9 is one of VideoToolbox's supplemental decoders, which a process has to +// ask for before AVFoundation can use one. Without this a `vp09` track in an +// MP4 opens and comes ready, and reports a picture of no size and hands out +// no frames, with no error anywhere. Asked once, and of nothing on a Mac that +// has no such decoder: its VP9 then fails as any format it cannot play does. +static void RegisterSupplementalDecoders() { + static dispatch_once_t once; + dispatch_once(&once, ^{ + VTRegisterSupplementalVideoDecoderIfAvailable(kCMVideoCodecType_VP9); + }); +} + static Napi::Value CreatePlayer(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); + RegisterSupplementalDecoders(); if (!info[0].IsString() || info[0].As().Utf8Value().empty()) { Napi::TypeError::New(env, "createPlayer(url, options): url is a path or a URL string") .ThrowAsJavaScriptException(); diff --git a/test/fixtures/clip-vp9.mp4 b/test/fixtures/clip-vp9.mp4 new file mode 100644 index 0000000..ff99078 Binary files /dev/null and b/test/fixtures/clip-vp9.mp4 differ diff --git a/test/player-vp9.js b/test/player-vp9.js new file mode 100644 index 0000000..e3f04eb --- /dev/null +++ b/test/player-vp9.js @@ -0,0 +1,77 @@ +'use strict'; +// A `vp09` track in an MP4 (src/player.mm, RegisterSupplementalDecoders): +// VP9 is a decoder VideoToolbox hands out only to a process that asks, and +// without one the item comes ready with a picture of no size and no frames +// ever, with no error. This is that picture having its size, and a seek +// landing on a frame. +// +// test/fixtures/clip-vp9.mp4 is half a second of one colour, 160x90 VP9, +// written by +// +// ffmpeg -f lavfi -i color=c=0xE07030:s=160x90:r=30:d=0.5 \ +// -c:v libvpx-vp9 -g 4 -tag:v vp09 -an -movflags +faststart clip-vp9.mp4 +// +// A Mac with no such decoder has nothing to show, and CI's virtual ones may +// be that: there, and only there, no picture is news and not a failure. +// +// Exits 0 when every expectation held. + +const path = require('path'); +const { native } = require('..'); + +const CLIP = path.join(__dirname, 'fixtures', 'clip-vp9.mp4'); + +const fail = (msg, ...rest) => { + console.error('player-vp9:', msg, ...rest); + process.exit(1); +}; + +function pumpUntil(pred, ms = 5000) { + return new Promise((resolve, reject) => { + const deadline = Date.now() + ms; + const tick = setInterval(() => { + native.pump2(); + if (pred()) { + clearInterval(tick); + resolve(); + } else if (Date.now() > deadline) { + clearInterval(tick); + reject(new Error('timed out')); + } + }, 8); + }); +} + +async function main() { + native.initApp(); + const events = []; + native.setBackendEventCallback((ev) => { + if (String(ev.type).startsWith('player-')) events.push(ev); + }); + const { id } = native.createPlayer(CLIP, { muted: true }); + const said = (type) => events.find((e) => e.id === id && e.type === type); + await pumpUntil(() => said('player-metadata') || said('player-error')).catch( + () => fail('the item never came ready'), + ); + if (said('player-error')) fail('the clip failed', said('player-error')); + // the item can come ready first and find its size after + await pumpUntil(() => events.some((e) => e.type === 'player-metadata' && e.width), 1500).catch(() => {}); + const sized = events.find((e) => e.type === 'player-metadata' && e.width); + if (!sized) { + if (process.env.CI) { + console.log('player-vp9: no VP9 decoder on this machine'); + process.exit(0); + } + fail('a picture of no size: nothing asked for the VP9 decoder'); + } + if (sized.width !== 160 || sized.height !== 90) fail('its size', sized); + native.playerSeek(id, 0.2); + await pumpUntil(() => events.some((e) => e.type === 'player-time' && e.currentTime > 0.15)).catch( + () => fail('the seek landed nowhere'), + ); + native.releasePlayer(id); + console.log('player-vp9: ok'); + process.exit(0); +} + +main().catch((err) => fail(err));