mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-15 23:03:14 -06:00
fix(tizen): portrait/flipped VIDEO via AVPlay (hardware-plane rotation) — no more black screen (#170)
Bold field report: portrait video on Tizen = black screen. Root cause: our orientation support rotates #stage with a CSS transform, but on Tizen the HTML5 <video> is composited on a HARDWARE video plane that ignores CSS rotate — so portrait/flipped video blacks out (images/widgets/text rotate fine). It's a Tizen platform limitation, not a regression. Fix: route portrait / portrait-flipped VIDEO through Tizen AVPlay, whose setDisplayRotation rotates the hardware plane itself. Scoped to portrait/flipped ONLY — landscape keeps the proven HTML5 <video> path (double-buffer + group-sync drift), so the working case is untouched. - index.html: <object id="avPlayer" type="application/avplayer"> hole-punch surface (hidden until a portrait video plays; off-hardware it's inert). - player.js: setOrientation()/avAvailable(); renderVideoAv() (open/setDisplayRect/ setDisplayRotation 90|270/setDisplayMethod LETTER_BOX/prepareAsync/play; onstreamcompleted -> loop|advance); avStop() torn down in clearStage() before every render; renderVideo() branches to AVPlay only for portrait/flipped when webapis.avplay exists. - Graceful: any AVPlay error/absence -> honest note (portrait_video_unsupported, 5 locales), NEVER a silent black screen. currentVideoEl stays null for AV (portrait is solo-first; schedule engine still drives index/position). - app.js: applyOrientation() now tells the player the orientation so renderVideo can choose the path. Exit-signal marker slice unaffected (test green). Landscape + off-hardware paths unchanged. NEEDS ON-DEVICE VALIDATION on a real Tizen TV (AVPlay can't be exercised off-hardware) — signed test .wgt built for Bold. Refs #170. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
40efaa1fed
commit
cc13352c58
|
|
@ -32,6 +32,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- #170: Tizen AVPlay video surface. HTML5 <video> lives on a hardware plane that ignores
|
||||||
|
CSS rotate, so portrait/flipped VIDEO blacks out; AVPlay's setDisplayRotation rotates the
|
||||||
|
hardware plane. Hidden until a portrait/flipped video plays; off-hardware (no webapis) it
|
||||||
|
stays unused. Sits above the (empty-during-AV) stage so the hole-punched plane is visible. -->
|
||||||
|
<object id="avPlayer" type="application/avplayer" style="position:fixed;inset:0;width:100%;height:100%;z-index:5;display:none"></object>
|
||||||
|
|
||||||
<!-- Playback stage -->
|
<!-- Playback stage -->
|
||||||
<div id="stage" class="screen stage hidden"></div>
|
<div id="stage" class="screen stage hidden"></div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
// packaged config.xml via the Tizen application API; fall back to a constant that
|
// packaged config.xml via the Tizen application API; fall back to a constant that
|
||||||
// build-wgt.sh stamps from config.xml's version="" so the dashboard always shows the
|
// build-wgt.sh stamps from config.xml's version="" so the dashboard always shows the
|
||||||
// version that is actually installed (never the old hardcoded '1.0.0').
|
// version that is actually installed (never the old hardcoded '1.0.0').
|
||||||
var APP_VERSION_FALLBACK = '1.9.2'; // st:app-version — stamped by build-wgt.sh
|
var APP_VERSION_FALLBACK = '1.9.5'; // st:app-version — stamped by build-wgt.sh
|
||||||
var APP_VERSION = (function () {
|
var APP_VERSION = (function () {
|
||||||
try {
|
try {
|
||||||
var v = tizen.application.getCurrentApplication().appInfo.version;
|
var v = tizen.application.getCurrentApplication().appInfo.version;
|
||||||
|
|
@ -582,6 +582,9 @@
|
||||||
// positions track the visible CONTENT, not the physical panel, in every orientation.
|
// positions track the visible CONTENT, not the physical panel, in every orientation.
|
||||||
orientEl(elStage.style, o);
|
orientEl(elStage.style, o);
|
||||||
if (elPip) orientEl(elPip.style, o);
|
if (elPip) orientEl(elPip.style, o);
|
||||||
|
// #170: the player needs the orientation so portrait/flipped VIDEO routes through AVPlay
|
||||||
|
// (hardware-plane rotation) instead of the CSS-rotated <video> that Tizen blacks out.
|
||||||
|
try { if (player && player.setOrientation) player.setOrientation(o); } catch (e) {}
|
||||||
}
|
}
|
||||||
function orientEl(s, o) {
|
function orientEl(s, o) {
|
||||||
if (!o || o === 'landscape') {
|
if (!o || o === 'landscape') {
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@
|
||||||
*/
|
*/
|
||||||
// Minimal i18n for the Tizen player (no shared i18n module here). Falls back to en.
|
// Minimal i18n for the Tizen player (no shared i18n module here). Falls back to en.
|
||||||
var TIZEN_I18N = {
|
var TIZEN_I18N = {
|
||||||
en: { nothing_scheduled: 'Nothing scheduled right now', no_content: 'No content assigned yet' },
|
en: { nothing_scheduled: 'Nothing scheduled right now', no_content: 'No content assigned yet', portrait_video_unsupported: 'Portrait video isn’t supported on this TV — use landscape, or pre-rotate the file and mark it Landscape.' },
|
||||||
es: { nothing_scheduled: 'No hay nada programado en este momento', no_content: 'Aún no hay contenido asignado' },
|
es: { nothing_scheduled: 'No hay nada programado en este momento', no_content: 'Aún no hay contenido asignado', portrait_video_unsupported: 'El vídeo en vertical no es compatible con este televisor: usa horizontal o rota el archivo y márcalo como Horizontal.' },
|
||||||
fr: { nothing_scheduled: 'Rien de programmé pour le moment', no_content: 'Aucun contenu attribué pour l’instant' },
|
fr: { nothing_scheduled: 'Rien de programmé pour le moment', no_content: 'Aucun contenu attribué pour l’instant', portrait_video_unsupported: 'La vidéo en portrait n’est pas prise en charge sur ce téléviseur — utilisez le paysage, ou faites pivoter le fichier et marquez-le Paysage.' },
|
||||||
de: { nothing_scheduled: 'Derzeit ist nichts geplant', no_content: 'Noch kein Inhalt zugewiesen' },
|
de: { nothing_scheduled: 'Derzeit ist nichts geplant', no_content: 'Noch kein Inhalt zugewiesen', portrait_video_unsupported: 'Hochformat-Video wird auf diesem TV nicht unterstützt – nutze Querformat oder drehe die Datei und markiere sie als Querformat.' },
|
||||||
pt: { nothing_scheduled: 'Nada programado no momento', no_content: 'Nenhum conteúdo atribuído ainda' }
|
pt: { nothing_scheduled: 'Nada programado no momento', no_content: 'Nenhum conteúdo atribuído ainda', portrait_video_unsupported: 'Vídeo em retrato não é suportado nesta TV — use paisagem ou gire o arquivo e marque-o como Paisagem.' }
|
||||||
};
|
};
|
||||||
var TZ_LANG = (function () { try { return (localStorage.getItem('rd_lang') || navigator.language || 'en').split('-')[0]; } catch (e) { return 'en'; } })();
|
var TZ_LANG = (function () { try { return (localStorage.getItem('rd_lang') || navigator.language || 'en').split('-')[0]; } catch (e) { return 'en'; } })();
|
||||||
function tzt(k) { return (TIZEN_I18N[TZ_LANG] && TIZEN_I18N[TZ_LANG][k]) || TIZEN_I18N.en[k] || k; }
|
function tzt(k) { return (TIZEN_I18N[TZ_LANG] && TIZEN_I18N[TZ_LANG][k]) || TIZEN_I18N.en[k] || k; }
|
||||||
|
|
@ -29,6 +29,10 @@ function PlaylistPlayer(stageEl, getBase) {
|
||||||
this.wallFollower = false; // video-wall: a follower holds the leader's item, no auto-advance
|
this.wallFollower = false; // video-wall: a follower holds the leader's item, no auto-advance
|
||||||
this.currentVideoEl = null; // current <video> (wall leader reads position; follower drift-corrects)
|
this.currentVideoEl = null; // current <video> (wall leader reads position; follower drift-corrects)
|
||||||
this.itemStartedAt = 0; // wall position fallback for non-video items
|
this.itemStartedAt = 0; // wall position fallback for non-video items
|
||||||
|
// #170: current device orientation. Portrait/flipped VIDEO must rotate the Tizen hardware video
|
||||||
|
// plane via AVPlay (CSS rotate can't touch it -> black screen). Set by app.js applyOrientation.
|
||||||
|
this.orientation = 'landscape';
|
||||||
|
this.avActive = false; // an AVPlay session is live (portrait video)
|
||||||
this.DEFAULT_DURATION = 10;
|
this.DEFAULT_DURATION = 10;
|
||||||
this.MIN_DURATION = 3;
|
this.MIN_DURATION = 3;
|
||||||
this.preloadEl = null; // #group-sync double buffer: pre-buffered next <video>
|
this.preloadEl = null; // #group-sync double buffer: pre-buffered next <video>
|
||||||
|
|
@ -153,6 +157,7 @@ PlaylistPlayer.prototype.stop = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
PlaylistPlayer.prototype.clearStage = function () {
|
PlaylistPlayer.prototype.clearStage = function () {
|
||||||
|
if (this.avActive) this.avStop(); // #170: tear down any AVPlay session (portrait video)
|
||||||
// Pause any video before removing so audio doesn't linger.
|
// Pause any video before removing so audio doesn't linger.
|
||||||
var v = this.stage.querySelector('video');
|
var v = this.stage.querySelector('video');
|
||||||
if (v) { try { v.pause(); v.removeAttribute('src'); v.load(); } catch (e) {} }
|
if (v) { try { v.pause(); v.removeAttribute('src'); v.load(); } catch (e) {} }
|
||||||
|
|
@ -347,7 +352,79 @@ PlaylistPlayer.prototype.renderImage = function (item, single) {
|
||||||
if (!single) this.schedule(this.durationMs(item));
|
if (!single) this.schedule(this.durationMs(item));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
PlaylistPlayer.prototype.setOrientation = function (o) { this.orientation = o || 'landscape'; };
|
||||||
|
PlaylistPlayer.prototype.avAvailable = function () { return !!(window.webapis && webapis.avplay); };
|
||||||
|
|
||||||
|
// #170: on Tizen the HTML5 <video> is composited on a hardware plane that ignores CSS rotate,
|
||||||
|
// so portrait/portrait-flipped orientation blacks the video out. AVPlay's setDisplayRotation
|
||||||
|
// rotates the hardware plane itself. We use it ONLY for portrait/flipped video — landscape keeps
|
||||||
|
// the proven <video> path (double-buffer + group-sync drift). Any AVPlay failure degrades to an
|
||||||
|
// honest note, never a silent black screen.
|
||||||
|
PlaylistPlayer.prototype.renderVideoAv = function (item, single) {
|
||||||
|
var self = this;
|
||||||
|
var url = this.contentUrl(item);
|
||||||
|
if (!url) { this.skipSoon(); return; }
|
||||||
|
var deg = this.orientation === 'portrait-flipped' ? 270 : 90;
|
||||||
|
var rot = deg === 270 ? 'PLAYER_DISPLAY_ROTATION_270' : 'PLAYER_DISPLAY_ROTATION_90';
|
||||||
|
var obj = document.getElementById('avPlayer');
|
||||||
|
var w = window.innerWidth || (window.screen && screen.width) || 1920;
|
||||||
|
var h = window.innerHeight || (window.screen && screen.height) || 1080;
|
||||||
|
// currentVideoEl stays null: wall/group-sync per-frame drift needs an HTML5 element; portrait
|
||||||
|
// signage is solo-first, and the schedule engine still drives index/position via gotoIndex.
|
||||||
|
this.currentVideoEl = null;
|
||||||
|
try { webapis.avplay.close(); } catch (e) {} // reset any prior session
|
||||||
|
try {
|
||||||
|
webapis.avplay.open(url);
|
||||||
|
if (obj) obj.style.display = 'block';
|
||||||
|
webapis.avplay.setDisplayRect(0, 0, w, h);
|
||||||
|
try { webapis.avplay.setDisplayMethod('PLAYER_DISPLAY_MODE_LETTER_BOX'); } catch (e) {}
|
||||||
|
webapis.avplay.setDisplayRotation(rot);
|
||||||
|
webapis.avplay.setListener({
|
||||||
|
onstreamcompleted: function () {
|
||||||
|
if (single) { try { webapis.avplay.seekTo(0); webapis.avplay.play(); } catch (e) { self.avStop(); self.skipSoon(); } }
|
||||||
|
else { self.advance(); }
|
||||||
|
},
|
||||||
|
onerror: function () { self.avFallback(item); }
|
||||||
|
});
|
||||||
|
this.avActive = true;
|
||||||
|
webapis.avplay.prepareAsync(
|
||||||
|
function () { try { webapis.avplay.play(); } catch (e) { self.avFallback(item); } },
|
||||||
|
function () { self.avFallback(item); }
|
||||||
|
);
|
||||||
|
// Safety net (mirrors the <video> path): advance after the known duration if
|
||||||
|
// onstreamcompleted never fires.
|
||||||
|
if (!single) {
|
||||||
|
var secs = Number(item.content_duration || item.duration_sec) || this.DEFAULT_DURATION;
|
||||||
|
this.schedule((secs + 5) * 1000);
|
||||||
|
}
|
||||||
|
} catch (e) { this.avFallback(item); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// AVPlay missing/failed on this device -> honest note (never a silent black), then move on.
|
||||||
|
PlaylistPlayer.prototype.avFallback = function (item) {
|
||||||
|
this.avStop();
|
||||||
|
this.stage.innerHTML =
|
||||||
|
'<div class="card" style="position:relative"><h1>ScreenTinker</h1>' +
|
||||||
|
'<p class="sub">' + tzt('portrait_video_unsupported') + '</p></div>';
|
||||||
|
// Don't wedge on a single looping item; re-check after a beat (or the item's duration).
|
||||||
|
this.schedule(Math.max(this.durationMs(item), 30000));
|
||||||
|
};
|
||||||
|
|
||||||
|
PlaylistPlayer.prototype.avStop = function () {
|
||||||
|
if (!this.avActive) return;
|
||||||
|
try { webapis.avplay.stop(); } catch (e) {}
|
||||||
|
try { webapis.avplay.close(); } catch (e) {}
|
||||||
|
var obj = document.getElementById('avPlayer');
|
||||||
|
if (obj) obj.style.display = 'none';
|
||||||
|
this.avActive = false;
|
||||||
|
};
|
||||||
|
|
||||||
PlaylistPlayer.prototype.renderVideo = function (item, single) {
|
PlaylistPlayer.prototype.renderVideo = function (item, single) {
|
||||||
|
// #170: portrait/flipped video must rotate the Tizen HARDWARE video plane, which a CSS transform
|
||||||
|
// on #stage can't do (black screen). Route it through AVPlay; landscape stays on <video>.
|
||||||
|
if ((this.orientation === 'portrait' || this.orientation === 'portrait-flipped') && this.avAvailable()) {
|
||||||
|
return this.renderVideoAv(item, single);
|
||||||
|
}
|
||||||
var self = this;
|
var self = this;
|
||||||
// Double buffer: reuse the pre-buffered element for this index if warmed (no black hold); its src
|
// Double buffer: reuse the pre-buffered element for this index if warmed (no black hold); its src
|
||||||
// is already set + buffering, so playback starts near-instantly.
|
// is already set + buffering, so playback starts near-instantly.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue