أنشئ APK مجانًا

واجهة JavaScript bridge API

الوسائط والبث

إشعار الوسائط على شاشة القفل الذي تتحكم به صفحتك، وتمرير فيديو إلى التلفاز.

navigator.mediaSession معيار الويب#

navigator.mediaSession + MediaMetadata

مثال

يعرض ما تشغّله صفحتك في إشعار وسائط وعلى شاشة القفل، مع أزرار السابق / تشغيل-إيقاف مؤقت / التالي / إيقاف التي تستدعي صفحتك. يستخدم واجهة Media Session API المعتادة.

القيمة المُعادة: لا شيء؛ ضغطات الأزرار تستدعي المعالجات التي عيّنتها باستخدام setActionHandler، مع { action } كما ينص المعيار. المتطلبات: فعّل Media Notification في الخطوة 4 (Access) عند الإنشاء. التطبيقات المبنية بالذكاء الاصطناعي يتم تفعيله لها تلقائيًا عندما يستخدمه الكود navigator.mediaSession.

var audio = document.getElementById('player');
var tracks = [
  { src: 'https://example.com/a.mp3', title: 'Morning', artist: 'Band', art: 'https://example.com/a.jpg' },
  { src: 'https://example.com/b.mp3', title: 'Evening', artist: 'Band', art: 'https://example.com/b.jpg' }
];
var index = 0;
var handlersReady = false;

function setupHandlers() {
  if (handlersReady || !('mediaSession' in navigator)) return;
  handlersReady = true;
  var ms = navigator.mediaSession;
  ms.setActionHandler('play', function () { audio.play(); });
  ms.setActionHandler('pause', function () { audio.pause(); });
  ms.setActionHandler('nexttrack', function () { playTrack(index + 1); });
  ms.setActionHandler('previoustrack', function () { playTrack(index - 1); });
  ms.setActionHandler('stop', function () { audio.pause(); audio.currentTime = 0; });
}

function playTrack(i) {
  index = (i + tracks.length) % tracks.length;
  var t = tracks[index];
  audio.src = t.src;
  audio.play();
  setupHandlers();   // on the first play, not at page load (see Notes)
  if ('mediaSession' in navigator && window.MediaMetadata) {
    navigator.mediaSession.metadata = new MediaMetadata({
      title: t.title, artist: t.artist, album: '',
      artwork: [{ src: t.art, sizes: '512x512', type: 'image/jpeg' }]
    });
  }
}

document.getElementById('play-btn').addEventListener('click', function () { playTrack(0); });

أبقِ زر التشغيل/الإيقاف المؤقت وشريط التقدم صحيحين:

function publishState() {
  if (!('mediaSession' in navigator)) return;
  navigator.mediaSession.playbackState = audio.paused ? 'paused' : 'playing';   // set this FIRST
  if (audio.duration > 0 && navigator.mediaSession.setPositionState) {
    navigator.mediaSession.setPositionState({
      duration: audio.duration, position: audio.currentTime, playbackRate: 1
    });
  }
}
audio.addEventListener('play', publishState);
audio.addEventListener('pause', publishState);
audio.addEventListener('seeked', publishState);

مشغّل بودكاست تتخطى فيه أزرار التالي / السابق في الإشعار 30 ثانية للأمام و15 ثانية للخلف بدلًا من تغيير الحلقة:

var audio = document.getElementById('player');
function skip(seconds) {
  var end = audio.duration || 0;
  audio.currentTime = Math.max(0, Math.min(end, audio.currentTime + seconds));
  if ('mediaSession' in navigator && navigator.mediaSession.setPositionState && end > 0) {
    navigator.mediaSession.setPositionState({ duration: end, position: audio.currentTime });
  }
}
if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('play', function () { audio.play(); });
  navigator.mediaSession.setActionHandler('pause', function () { audio.pause(); });
  navigator.mediaSession.setActionHandler('nexttrack', function () { skip(30); });
  navigator.mediaSession.setActionHandler('previoustrack', function () { skip(-15); });
  if (window.MediaMetadata) {
    navigator.mediaSession.metadata = new MediaMetadata({
      title: 'Episode 12: Getting started', artist: 'My Podcast',
      artwork: [{ src: 'https://example.com/podcast-512.jpg', sizes: '512x512' }]
    });
  }
}

ملاحظات:

  • في التطبيق، يُثبَّت navigator.mediaSession قبل أول سكربت في الصفحة، فيمكن ضبط المعالجات والبيانات الوصفية في أي وقت - أعلى الصفحة أو عند بدء التشغيل. وهو موجود في الصفحة الرئيسية، لا داخل iframes.
  • الإجراءات المدعومة: play، pause، nexttrack، previoustrack، stop. التقديم من الإشعار غير مدعوم، لذا لا يُطلق seekto أبدًا.
  • ضبط playbackState يُبقي الموضع من آخر استدعاء لـ setPositionState؛ استدعِ setPositionState مجددًا عندما يتغير الموضع فعلًا (قفز).
  • تُستخدم صورة الغلاف الأخيرة فقط. يجب أن تكون عنوان https:// كاملًا على الإنترنت (يحمّلها التطبيق، بحد 4 ثوانٍ)؛ الملف داخل ملف ZIP الخاص بتطبيقك لا يظهر.
  • <audio> / <video> تُتتبَّع العناصر تلقائيًا: عندما تعالج الصفحة زرًا لا بـ setActionHandler ولا بـ window.WebToApkOnMediaAction الخاص بها، تتحكم تشغيل / إيقاف مؤقت / إيقاف في عنصر الوسائط الذي يعمل (وإلا آخر ما شُغّل، وإلا الأول).
  • لإزالة الإشعار، استدعِ WebToApk.clearMediaNotification() (ضبط metadata = null لا يزيله).
  • الصفحة التي تعرّف window.WebToApkOnMediaAction = function (action) { ... } (الإجراءات play، pause، next، prev، stop) تتلقى كل ضغطة زر هناك أيضًا - ولم يعد التطبيق يستبدلها. استخدم أحدهما لا كليهما، وإلا تُنفَّذ الضغطة مرتين.

requestPictureInPicture معيار الويب#

video.requestPictureInPicture() — Picture-in-Picture

مثال

واجهة الصورة داخل الصورة القياسية: اجعل فيديو يطفو في نافذة صغيرة بينما يفعل المستخدم شيئًا آخر. Chrome على Android يدعمها وWebView في Android لا؛ وفي التطبيق يملأ الفيديو النافذة العائمة كما في Chrome.

يعيد: Promise لـ PictureInPictureWindow (أحداث width وheight وresize). يتلقى الفيديو enterpictureinpicture، ثم leavepictureinpicture عندما يغلق المستخدم النافذة أو يوسّعها؛ وdocument.pictureInPictureElement هو الفيديو في هذه الأثناء. يُرفض بـ NotAllowedError دون نقرة، وInvalidStateError قبل تحميل بيانات الفيديو الوصفية أو عندما يكون disablePictureInPicture. يتطلب: تفعيل الصورة داخل الصورة عند الإنشاء (تُفعَّل تلقائيًا عندما تستدعي شيفرتك requestPictureInPicture)، وAndroid 8 أو أحدث.

<video id="v" src="clip.mp4" controls playsinline></video>
<button id="pip">Pop out</button>
<script>
const v = document.getElementById('v');
const btn = document.getElementById('pip');
btn.hidden = !document.pictureInPictureEnabled;
btn.addEventListener('click', async () => {
  try {
    if (document.pictureInPictureElement) await document.exitPictureInPicture();
    else await v.requestPictureInPicture();
  } catch (e) {
    console.warn('Picture-in-Picture:', e.name);
  }
});
v.addEventListener('enterpictureinpicture', () => { btn.textContent = 'Back to app'; });
v.addEventListener('leavepictureinpicture', () => { btn.textContent = 'Pop out'; });
</script>

الخروج تلقائيًا إلى نافذة عائمة عندما يغادر المستخدم، والإبلاغ عن نسخة غير مدعومة:

const v = document.getElementById('v');
if (!document.pictureInPictureEnabled || v.disablePictureInPicture) {
  document.getElementById('pip').hidden = true;      // this build has no picture-in-picture
}
document.addEventListener('visibilitychange', async () => {
  if (document.hidden && !v.paused && !document.pictureInPictureElement) {
    try { await v.requestPictureInPicture(); }
    catch (e) { console.warn(e.name); }               // NotAllowedError without a tap
  }
});

انتظر البيانات الوصفية، وغيّر الحجم مع النافذة:

async function popOut(video) {
  if (video.readyState < 1) await new Promise((r) => video.addEventListener('loadedmetadata', r, { once: true }));
  try {
    const pipWin = await video.requestPictureInPicture();
    pipWin.addEventListener('resize', () => console.log(pipWin.width, pipWin.height));
  } catch (e) {
    if (e.name === 'InvalidStateError') showMessage('This video cannot float.');
    else if (e.name === 'NotAllowedError') showMessage('Tap the button to pop out.');
  }
}
document.getElementById('pip').onclick = () => popOut(document.getElementById('v'));

ملاحظات: يضع Android التطبيق كاملًا في النافذة العائمة، لذلك يُعرض الفيديو أولًا وحده (بملء الشاشة) وتتخذ النافذة شكل الفيديو؛ وعند مغادرة النافذة يعود الفيديو إلى الصفحة. يعيد document.exitPictureInPicture() التطبيق إلى ملء الشاشة. عندما لا تتوفر في النسخة المبنية ميزة الصورة داخل الصورة، تكون قيمة document.pictureInPictureEnabled هي false، كما في متصفح لا يدعمها. يصل تغيير الوضع إلى الصفحة على شكل appmint:pip. وسائل النقل الداخلية: __pipEnter, __pipExit, __pipAvailable, __pipPrepare.

__pipAvailable الجسر (bridge)#

window.WebToApk.__pipAvailable(): Boolean

هل يمكن أن يعمل video.requestPictureInPicture(): وضع PiP مفعّل في النسخة المبنية، وAndroid 8 أو أحدث.

مثال

موثَّق مع WebToApk.__pipEnter - والمثال هناك يعرض هذا أيضًا.

نقل داخلي خلف video.requestPictureInPicture() وdocument.exitPictureInPicture(). تستخدم الصفحات الواجهة القياسية؛ ولا تستدعِ هذه مباشرة، فطرق __ قد تتغير دون إشعار.

يعيد: يعيد __pipEnter(width, height) القيمة "ok" عندما قبل Android النافذة (بنسبة أبعاد الفيديو)، وإلا "unsupported" / "failed". يصل فتح النافذة وإغلاقها كـ appmint:pip مع {inPip}. و__pipAvailable() صحيح عندما يكون الإنشاء مفعّلًا فيه الصورة داخل الصورة على Android 8+. يتطلب: تفعيل الصورة داخل الصورة عند الإنشاء.

استخدم الواجهة العامة:

const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());

تحقق من الدعم أولًا، وأبلغ عن الرفض:

async function floatVideo(video) {
  if (!document.pictureInPictureEnabled) { showMessage('Picture-in-picture is not on in this app.'); return; }
  try {
    await video.requestPictureInPicture();
  } catch (err) {
    showMessage('Could not open the floating window.');
  }
}

اتركها مجددًا وتابع الحالة بالأحداث القياسية:

video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };

ملاحظات: منفّذ في غلاف التطبيق (PictureInPictureShim.kt). يمنع __pipPrepare الغلاف من تدوير الشاشة أفقيًا لخطوة ملء الشاشة أثناء الانتقال إلى النافذة.

__pipEnter الجسر (bridge)#

window.WebToApk.__pipEnter(width: Int, height: Int): String

يفتح نافذة PiP بنسبة أبعاد فيديو [width]×[height]: "ok"، أو "unsupported" / "failed". يصل تغيير الوضع على شكل `appmint:pip`.

مثال

نقل داخلي خلف video.requestPictureInPicture() وdocument.exitPictureInPicture(). تستخدم الصفحات الواجهة القياسية؛ ولا تستدعِ هذه مباشرة، فطرق __ قد تتغير دون إشعار.

يعيد: يعيد __pipEnter(width, height) القيمة "ok" عندما قبل Android النافذة (بنسبة أبعاد الفيديو)، وإلا "unsupported" / "failed". يصل فتح النافذة وإغلاقها كـ appmint:pip مع {inPip}. و__pipAvailable() صحيح عندما يكون الإنشاء مفعّلًا فيه الصورة داخل الصورة على Android 8+. يتطلب: تفعيل الصورة داخل الصورة عند الإنشاء.

استخدم الواجهة العامة:

const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());

تحقق من الدعم أولًا، وأبلغ عن الرفض:

async function floatVideo(video) {
  if (!document.pictureInPictureEnabled) { showMessage('Picture-in-picture is not on in this app.'); return; }
  try {
    await video.requestPictureInPicture();
  } catch (err) {
    showMessage('Could not open the floating window.');
  }
}

اتركها مجددًا وتابع الحالة بالأحداث القياسية:

video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };

ملاحظات: منفّذ في غلاف التطبيق (PictureInPictureShim.kt). يمنع __pipPrepare الغلاف من تدوير الشاشة أفقيًا لخطوة ملء الشاشة أثناء الانتقال إلى النافذة.

__pipExit الجسر (bridge)#

window.WebToApk.__pipExit()

document.exitPictureInPicture(): يعيد التطبيق إلى ملء الشاشة.

مثال

موثَّق مع WebToApk.__pipEnter - والمثال هناك يعرض هذا أيضًا.

نقل داخلي خلف video.requestPictureInPicture() وdocument.exitPictureInPicture(). تستخدم الصفحات الواجهة القياسية؛ ولا تستدعِ هذه مباشرة، فطرق __ قد تتغير دون إشعار.

يعيد: يعيد __pipEnter(width, height) القيمة "ok" عندما قبل Android النافذة (بنسبة أبعاد الفيديو)، وإلا "unsupported" / "failed". يصل فتح النافذة وإغلاقها كـ appmint:pip مع {inPip}. و__pipAvailable() صحيح عندما يكون الإنشاء مفعّلًا فيه الصورة داخل الصورة على Android 8+. يتطلب: تفعيل الصورة داخل الصورة عند الإنشاء.

استخدم الواجهة العامة:

const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());

تحقق من الدعم أولًا، وأبلغ عن الرفض:

async function floatVideo(video) {
  if (!document.pictureInPictureEnabled) { showMessage('Picture-in-picture is not on in this app.'); return; }
  try {
    await video.requestPictureInPicture();
  } catch (err) {
    showMessage('Could not open the floating window.');
  }
}

اتركها مجددًا وتابع الحالة بالأحداث القياسية:

video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };

ملاحظات: منفّذ في غلاف التطبيق (PictureInPictureShim.kt). يمنع __pipPrepare الغلاف من تدوير الشاشة أفقيًا لخطوة ملء الشاشة أثناء الانتقال إلى النافذة.

__pipPrepare الجسر (bridge)#

window.WebToApk.__pipPrepare()

الصفحة على وشك وضع فيديو بملء الشاشة لأجل PiP: لا تفرض الوضع الأفقي لذلك.

مثال

موثَّق مع WebToApk.__pipEnter - والمثال هناك يعرض هذا أيضًا.

نقل داخلي خلف video.requestPictureInPicture() وdocument.exitPictureInPicture(). تستخدم الصفحات الواجهة القياسية؛ ولا تستدعِ هذه مباشرة، فطرق __ قد تتغير دون إشعار.

يعيد: يعيد __pipEnter(width, height) القيمة "ok" عندما قبل Android النافذة (بنسبة أبعاد الفيديو)، وإلا "unsupported" / "failed". يصل فتح النافذة وإغلاقها كـ appmint:pip مع {inPip}. و__pipAvailable() صحيح عندما يكون الإنشاء مفعّلًا فيه الصورة داخل الصورة على Android 8+. يتطلب: تفعيل الصورة داخل الصورة عند الإنشاء.

استخدم الواجهة العامة:

const video = document.querySelector('video');
button.addEventListener('click', () => video.requestPictureInPicture());

تحقق من الدعم أولًا، وأبلغ عن الرفض:

async function floatVideo(video) {
  if (!document.pictureInPictureEnabled) { showMessage('Picture-in-picture is not on in this app.'); return; }
  try {
    await video.requestPictureInPicture();
  } catch (err) {
    showMessage('Could not open the floating window.');
  }
}

اتركها مجددًا وتابع الحالة بالأحداث القياسية:

video.addEventListener('enterpictureinpicture', () => hideControls());
video.addEventListener('leavepictureinpicture', () => showControls());
closeBtn.onclick = () => { if (document.pictureInPictureElement) document.exitPictureInPicture(); };

ملاحظات: منفّذ في غلاف التطبيق (PictureInPictureShim.kt). يمنع __pipPrepare الغلاف من تدوير الشاشة أفقيًا لخطوة ملء الشاشة أثناء الانتقال إلى النافذة.

castMedia الجسر (bridge)#

window.WebToApk.castMedia(mediaUrl: String)

يعرض [mediaUrl] على أي شيء في هذا الهاتف يستطيع تشغيله على شاشة أخرى - Chromecast، تطبيق تلفاز ذكي، مشغّل محلي - عبر قائمة اختيار النظام. يُؤخذ النوع من العنوان ([CastMediaType]): ملف صوتي (.mp3، .m4a، .aac، .ogg، .opus، .wav، .flac …) أو عنوان `data:` صوتي يُعرض على تطبيقات الصوت، وأي شيء آخر - ملفات الفيديو، قوائم البث، العناوين بلا امتداد - على تطبيقات الفيديو. هذا تسليم وليس جلسة بث: لا يبقى التطبيق متحكمًا بالتشغيل، ولا يوجد رد نداء. الصفحة التي تحتاج جلسة حقيقية يجب أن تستخدم Cast SDK في JavaScript بدلًا من ذلك.

مثال

يعرض عنوان فيديو أو صوت على التطبيقات في الهاتف القادرة على تشغيله على شاشة أخرى (تطبيق Chromecast، تطبيق تلفاز ذكي، مشغّل فيديو)، عبر قائمة اختيار "بث إلى..." في Android.

القيمة المُعادة: لا شيء، ولا دالة رد. المتطلبات: لا شيء لتفعيله. يحتاج الهاتف إلى تطبيق يقبل الرابط.

function castToTv(url) {
  if (window.WebToApk && typeof window.WebToApk.castMedia === 'function') {
    window.WebToApk.castMedia(url);          // opens the "Cast to..." chooser
  } else {
    alert('Casting is only available in the app');
  }
}

document.getElementById('cast-btn').addEventListener('click', function () {
  var video = document.querySelector('video');
  var url = video && video.currentSrc ? video.currentSrc : 'https://example.com/movie.mp4';
  castToTv(url);
});

أرسل حلقة بودكاست من قائمة إلى مكبّر صوت أو تطبيق تلفاز (رابط .mp3 يُعرض على تطبيقات الصوت):

// <li><button class="cast" data-src="https://example.com/ep12.mp3">Cast</button> Episode 12</li>
document.getElementById('episodes').addEventListener('click', function (e) {
  var btn = e.target.closest('button.cast');
  if (!btn) return;
  var src = btn.getAttribute('data-src') || '';
  if (src.indexOf('https://') !== 0) {
    alert('This episode cannot be cast');   // files inside the app cannot be handed over
    return;
  }
  if (window.WebToApk && typeof window.WebToApk.castMedia === 'function') {
    window.WebToApk.castMedia(src);
  }
});

أظهر زر Cast داخل التطبيق فقط، وأوقف مشغّل الصفحة نفسه مؤقتًا قبل تمرير بث مباشر:

var castBtn = document.getElementById('cast-live');
var canCast = !!(window.WebToApk && typeof window.WebToApk.castMedia === 'function');
castBtn.hidden = !canCast;

castBtn.addEventListener('click', function () {
  var video = document.getElementById('live');
  video.pause();                                   // avoid playing twice, here and on the TV
  window.WebToApk.castMedia('https://example.com/live/stream.m3u8');   // offered to video apps
});

ملاحظات:

  • هذا تمرير وليس جلسة بث: صفحتك لا تتحكم في التشغيل بعد أن يختار المستخدم تطبيقًا.
  • يجب أن يكون العنوان رابط https:// كاملًا تستطيع تطبيقات أخرى فتحه. ملف داخل ZIP تطبيقك أو عنوان blob: أو عنوان صفحة ويب لن يعمل.
  • يأتي النوع من الرابط: ملف صوتي (.mp3، .m4a، .aac، .ogg، .opus، .wav، .flac …) أو عنوان audio/ data: يُعرض على تطبيقات الصوت؛ وأي شيء آخر - ملفات الفيديو، بث .m3u8، الروابط بلا امتداد ملف - على تطبيقات الفيديو.

clearMediaNotification الجسر (bridge)#

window.WebToApk.clearMediaNotification()

يتيح لمحتوى الويب التحكم في إشعار Android بنمط الوسائط وتلقي استدعاءات أزرار التحكم (تشغيل/إيقاف مؤقت/التالي/السابق) من شاشة القفل ولوحة الإشعارات. الاستخدام من JavaScript: window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // playing, pos ms, dur ms window.WebToApk.clearMediaNotification() الاستدعاء من Android → JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

موصوف ضمن مجموعته، جسر إشعارات الوسائط، وليس وحده.

مثال

يزيل إشعار الوسائط وعناصر تحكم شاشة القفل، مثلًا عندما يغلق المستخدم مشغّلك.

يعيد: لا شيء. يتطلب: فعّل إشعار الوسائط في الخطوة 4 (الوصول) عند الإنشاء. بدونه لا يفعل الاستدعاء شيئًا.

لا يوجد استدعاء ويب قياسي لهذا، فاستخدم الجسر:

function closePlayer() {
  var audio = document.getElementById('player');
  audio.pause();
  audio.currentTime = 0;
  if (window.WebToApk && typeof window.WebToApk.clearMediaNotification === 'function') {
    window.WebToApk.clearMediaNotification();
  }
}

document.getElementById('close-player').addEventListener('click', closePlayer);

// The notification's own Stop button already removes it; just stop your audio:
if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('stop', function () {
    var audio = document.getElementById('player');
    audio.pause();
    audio.currentTime = 0;
  });
}

مؤقّت نوم يوقف الصوت ويزيل الإشعار بعد عدد الدقائق الذي تختاره:

var sleepTimer = null;
function setSleepTimer(minutes) {
  clearTimeout(sleepTimer);
  sleepTimer = setTimeout(function () {
    var audio = document.getElementById('player');
    audio.pause();
    if (window.WebToApk && typeof window.WebToApk.clearMediaNotification === 'function') {
      window.WebToApk.clearMediaNotification();
    }
  }, minutes * 60 * 1000);
}
document.getElementById('sleep-30').addEventListener('click', function () { setSleepTimer(30); });

أزل الإشعار عند انتهاء آخر مقطع في قائمة التشغيل، بدل ترك زر تشغيل بلا فائدة:

var audio = document.getElementById('player');
var playlist = ['https://example.com/1.mp3', 'https://example.com/2.mp3'];
var current = 0;

audio.addEventListener('ended', function () {
  current++;
  if (current < playlist.length) {
    audio.src = playlist[current];
    audio.play();
  } else if (window.WebToApk && typeof window.WebToApk.clearMediaNotification === 'function') {
    window.WebToApk.clearMediaNotification();
  }
});

ملاحظات: استخدم هذا لـ"الإيقاف"، وليس لـ"الإيقاف المؤقت" (للإيقاف المؤقت أرسل setPlaybackState(false, ...) ليبقى زر التشغيل). تعيين navigator.mediaSession.metadata = null لا يزيل الإشعار. استدعاء setMediaMetadata / setPlaybackState التالي (أو بيانات / حالة navigator.mediaSession جديدة) يعيد الإشعار وعناصر التحكم على شاشة القفل.

setMediaMetadata الجسر (bridge)#

window.WebToApk.setMediaMetadata(title: String, artist: String, artworkUrl: String)

يتيح لمحتوى الويب التحكم في إشعار Android بنمط الوسائط وتلقي استدعاءات أزرار التحكم (تشغيل/إيقاف مؤقت/التالي/السابق) من شاشة القفل ولوحة الإشعارات. الاستخدام من JavaScript: window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // playing, pos ms, dur ms window.WebToApk.clearMediaNotification() الاستدعاء من Android → JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

موصوف ضمن مجموعته، جسر إشعارات الوسائط، وليس وحده.

مثال

يضبط العنوان والفنان وصورة الغلاف لإشعار الوسائط ومشغّل شاشة القفل. تضبط الصفحات عادةً navigator.mediaSession.metadata، الذي يستدعي هذا نيابةً عنك.

يعيد: لا شيء. يتطلب: فعّل إشعار الوسائط في الخطوة 4 (الوصول) عند الإنشاء. بدونه لا يفعل الاستدعاء شيئًا.

الطريقة العامة (استخدم هذه):

if ('mediaSession' in navigator && window.MediaMetadata) {
  navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Episode 12', artist: 'My Podcast',
    artwork: [{ src: 'https://example.com/cover.jpg', sizes: '512x512' }]
  });
}

الاستدعاء الخام. الوسائط: title, artist, artworkUrl (استخدم '' لعدم وجود صورة):

function showNowPlaying(track) {
  if (!(window.WebToApk && typeof window.WebToApk.setMediaMetadata === 'function')) return;
  window.WebToApk.setMediaMetadata(track.title, track.artist || '', track.cover || '');
  window.WebToApk.setPlaybackState(true, 0, Math.round(track.seconds * 1000));
}

// Buttons in the notification come back through the standard API:
if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('nexttrack', function () { playNext(); });
  navigator.mediaSession.setActionHandler('previoustrack', function () { playPrevious(); });
}

أبقِ عنوان الإشعار محدّثًا لبث إذاعي مباشر، حيث تتغير الأغنية دون أن تحمّل الصفحة ملفًا جديدًا:

var lastSong = '';
function refreshNowPlaying() {
  fetch('https://example.com/radio/now-playing.json')
    .then(function (r) { return r.json(); })
    .then(function (np) {            // e.g. { "title": "...", "artist": "...", "cover": "https://..." }
      var key = np.title + '|' + np.artist;
      if (key === lastSong) return;  // only update when the song really changed
      lastSong = key;
      if (window.WebToApk && typeof window.WebToApk.setMediaMetadata === 'function') {
        window.WebToApk.setMediaMetadata(np.title || 'Live radio', np.artist || '', np.cover || '');
      }
    })
    .catch(function () { /* try again on the next tick */ });
}
document.getElementById('radio').addEventListener('play', function () {
  refreshNowPlaying();
  setInterval(refreshNowPlaying, 30000);
}, { once: true });

ملاحظات:

  • يظهر الإشعار (أو يتحدّث) بهذا الاستدعاء. العنوان الفارغ يعرض اسم التطبيق.
  • artworkUrl يجب أن يكون عنوانًا كاملًا بـ https:// على الإنترنت. ينزّله التطبيق (بحد 4 ثوانٍ)؛ وإن فشل تُعرض أيقونة التطبيق.
  • استقبل ضغطات الأزرار بـ navigator.mediaSession.setActionHandler (انظر navigator.mediaSession)، أو بـ window.WebToApkOnMediaAction = function (action) { ... } الخاص بك (play، pause، next، prev، stop) - استخدم أحدهما. بعد clearMediaNotification()، يعيد الاستدعاء التالي لهذا (أو لـ setPlaybackState) الإشعار وعناصر تحكم شاشة القفل.

setPlaybackState الجسر (bridge)#

window.WebToApk.setPlaybackState(isPlaying: Boolean, positionMs: Long, durationMs: Long)

يتيح لمحتوى الويب التحكم في إشعار Android بنمط الوسائط وتلقي استدعاءات أزرار التحكم (تشغيل/إيقاف مؤقت/التالي/السابق) من شاشة القفل ولوحة الإشعارات. الاستخدام من JavaScript: window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg') window.WebToApk.setPlaybackState(true, 30000, 240000) // playing, pos ms, dur ms window.WebToApk.clearMediaNotification() الاستدعاء من Android → JS: window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

موصوف ضمن مجموعته، جسر إشعارات الوسائط، وليس وحده.

مثال

يخبر إشعار الوسائط ما إذا كنت تشغّل أو متوقفًا مؤقتًا، وأين أنت في المقطع. تضبط الصفحات عادة navigator.mediaSession.playbackState وتستدعي setPositionState، وهما يستدعيان هذا نيابةً عنك.

يعيد: لا شيء. يتطلب: فعّل إشعار الوسائط في الخطوة 4 (الوصول) عند الإنشاء. بدونه لا يفعل الاستدعاء شيئًا.

الطريقة العامة (استخدم هذه):

var audio = document.getElementById('player');
function publish() {
  if (!('mediaSession' in navigator)) return;
  navigator.mediaSession.playbackState = audio.paused ? 'paused' : 'playing';
  if (audio.duration > 0 && navigator.mediaSession.setPositionState) {
    navigator.mediaSession.setPositionState({ duration: audio.duration, position: audio.currentTime });
  }
}
audio.addEventListener('play', publish);
audio.addEventListener('pause', publish);

الاستدعاء الخام. الوسائط: isPlaying (true/false)، positionMs، durationMs (بالمللي ثانية الكاملة):

function publishRaw(audio) {
  if (!(window.WebToApk && typeof window.WebToApk.setPlaybackState === 'function')) return;
  window.WebToApk.setPlaybackState(
    !audio.paused,
    Math.floor(audio.currentTime * 1000),
    Math.floor((audio.duration || 0) * 1000)
  );
}

أبلغ عن الموضع الجديد بعد أن يقفز المستخدم، وبدّل الإشعار إلى تشغيل عند انتهاء المقطع:

var audio = document.getElementById('player');
function report(playing) {
  if (!(window.WebToApk && typeof window.WebToApk.setPlaybackState === 'function')) return;
  var durMs = Math.floor((audio.duration || 0) * 1000);
  var posMs = Math.floor(audio.currentTime * 1000);
  window.WebToApk.setPlaybackState(playing, Math.min(posMs, durMs || posMs), durMs);
}
audio.addEventListener('seeked', function () { report(!audio.paused); });
audio.addEventListener('ended', function () { report(false); });   // Play button, notification can be swiped away

ملاحظات: يُظهر true زر الإيقاف المؤقت ويمنع إزالة الإشعار بالسحب؛ ويُظهر false زر التشغيل. استدعِ setMediaMetadata أولًا ليكون للإشعار عنوان. لإزالة الإشعار عندما يوقف المستخدم التشغيل، استدعِ clearMediaNotification().

يتم إنشاؤه من بيئة تشغيل التطبيق وملفات الأمثلة الخاصة به عند كل بناء للمستندات. اقرأه بصيغة Markdown · جميع العائلات.

تمت المراجعة مقابل الجسر المُرفق بتاريخ 2026-10-22.