تخصيص إشعارات الوسائط والتعامل مع قوائم التشغيل

François Beaufort
François Beaufort

باستخدام واجهة برمجة التطبيقات الجديدة Media Session API، يمكنك الآن تخصيص إعلامات الوسائط من خلال تقديم بيانات وصفية للوسائط التي يشغّلها تطبيق الويب. ويتيح لك أيضًا معالجة الأحداث المتعلّقة بالوسائط، مثل التقديم أو الترجيع أو تغيير المقطع الصوتي، والتي قد تأتي من الإشعارات أو مفاتيح الوسائط. هل أنت متحمس؟ جرِّب عيّنات جلسات الوسائط الرسمية.

تتوفّر Media Session API في الإصدار 57 من Chrome (الإصدار التجريبي في شباط/فبراير 2017 والإصدار الثابت في آذار/مارس 2017).

النص المختصر (TL;DR) عن جلسة الوسائط
الصورة تابعة لميشاال ألّو-نيلسن / ترخيص Creative Commons BY 2.0

Gimme what I want

هل لديك معرفة مسبقة بواجهة برمجة التطبيقات Media Session API وهل تريد ببساطة نسخ ولصق بعض الرموز البرمجية المتكررة بدون أي تردد؟ إليك التفاصيل.

if ('mediaSession' in navigator) {

    navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    artist: 'Rick Astley',
    album: 'Whenever You Need Somebody',
    artwork: [
        { src: 'https://dummyimage.com/96x96',   sizes: '96x96',   type: 'image/png' },
        { src: 'https://dummyimage.com/128x128', sizes: '128x128', type: 'image/png' },
        { src: 'https://dummyimage.com/192x192', sizes: '192x192', type: 'image/png' },
        { src: 'https://dummyimage.com/256x256', sizes: '256x256', type: 'image/png' },
        { src: 'https://dummyimage.com/384x384', sizes: '384x384', type: 'image/png' },
        { src: 'https://dummyimage.com/512x512', sizes: '512x512', type: 'image/png' },
    ]
    });

    navigator.mediaSession.setActionHandler('play', function() {});
    navigator.mediaSession.setActionHandler('pause', function() {});
    navigator.mediaSession.setActionHandler('seekbackward', function() {});
    navigator.mediaSession.setActionHandler('seekforward', function() {});
    navigator.mediaSession.setActionHandler('previoustrack', function() {});
    navigator.mediaSession.setActionHandler('nexttrack', function() {});
}

الاطّلاع على الرمز

لنبدأ اللعب 🎷

أضِف عنصر <audio> بسيطًا إلى صفحة الويب وحدِّد عدة مصادر وسائط لكي تتمكّن المتصفّح من اختيار المصدر الأنسب.

<audio controls>
    <source src="audio.mp3" type="audio/mp3"/>
    <source src="audio.ogg" type="audio/ogg"/>
</audio>

كما تعلم، تم إيقاف autoplay لعناصر الصوت على Chrome لأجهزة Android، ما يعني أنّنا يجب أن نستخدم طريقة play() لعنصر الصوت. يجب بدء هذه الطريقة من خلال إيماءة المستخدم، مثل لمسة أو نقرة بالماوس. ويعني ذلك الاستماع إلى أحداث pointerup وclick وtouchend. بعبارة أخرى، على المستخدم النقر على زر قبل أن يصدر تطبيق الويب ضوضاء.

playButton.addEventListener('pointerup', function(event) {
    let audio = document.querySelector('audio');

    // User interacted with the page. Let's play audio...
    audio.play()
    .then(_ => { /* Set up media session... */ })
    .catch(error => { console.log(error) });
});

إذا كنت لا تريد تشغيل الصوت مباشرةً بعد التفاعل الأول، ننصحك باستخدام طريقة load() لعنصر الصوت. وهذه إحدى الطرق التي يتتبّع بها المتصفّح ما إذا كان المستخدم قد تفاعل مع العنصر. يُرجى العلم أنّ ذلك قد يساعد أيضًا في تشغيل المحتوى بسلاسة لأنّه سيتم تحميله مسبقًا.

let audio = document.querySelector('audio');

welcomeButton.addEventListener('pointerup', function(event) {
  // User interacted with the page. Let's load audio...
  <strong>audio.load()</strong>
  .then(_ => { /* Show play button for instance... */ })
  .catch(error => { console.log(error) });
});

// Later...
playButton.addEventListener('pointerup', function(event) {
  <strong>audio.play()</strong>
  .then(_ => { /* Set up media session... */ })
  .catch(error => { console.log(error) });
});

تخصيص الإشعار

عندما يشغّل تطبيق الويب الخاص بك محتوى صوتيًا، يمكنك رؤية إشعار وسائط في قائمة الإشعارات. على أجهزة Android، يبذل Chrome قصارى جهده لعرض المعلومات المناسبة باستخدام عنوان المستند والرمز الأكبر الذي يمكنه العثور عليه.

بدون جلسة بث الوسائط
بدون جلسة وسائط
مع جلسة بث الوسائط
مع جلسة وسائط

ضبط البيانات الوصفية

لنطّلِع على كيفية تخصيص إشعار الوسائط هذا من خلال ضبط بعض البيانات الوصفية لجلسة الوسائط، مثل العنوان والفنان واسم الألبوم والعمل الفني باستخدام واجهة برمجة التطبيقات Media Session API.

// When audio starts playing...
if ('mediaSession' in navigator) {

    navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    artist: 'Rick Astley',
    album: 'Whenever You Need Somebody',
    artwork: [
        { src: 'https://dummyimage.com/96x96',   sizes: '96x96',   type: 'image/png' },
        { src: 'https://dummyimage.com/128x128', sizes: '128x128', type: 'image/png' },
        { src: 'https://dummyimage.com/192x192', sizes: '192x192', type: 'image/png' },
        { src: 'https://dummyimage.com/256x256', sizes: '256x256', type: 'image/png' },
        { src: 'https://dummyimage.com/384x384', sizes: '384x384', type: 'image/png' },
        { src: 'https://dummyimage.com/512x512', sizes: '512x512', type: 'image/png' },
    ]
    });
}

بعد انتهاء التشغيل، ليس عليك "إلغاء" جلسة الوسائط لأنّه سيختفي الإشعار تلقائيًا. يُرجى العِلم أنّه سيتم استخدام قيمة navigator.mediaSession.metadata الحالية عند بدء أي تشغيل. لهذا السبب، يجب تعديلها للتأكّد من عرض معلومات مفيدة دائمًا في إشعار الوسائط.

المقطع الصوتي السابق / المقطع الصوتي التالي

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

let audio = document.createElement('audio');

let playlist = ['audio1.mp3', 'audio2.mp3', 'audio3.mp3'];
let index = 0;

navigator.mediaSession.setActionHandler('previoustrack', function() {
    // User clicked "Previous Track" media notification icon.
    index = (index - 1 + playlist.length) % playlist.length;
    playAudio();
});

navigator.mediaSession.setActionHandler('nexttrack', function() {
    // User clicked "Next Track" media notification icon.
    index = (index + 1) % playlist.length;
    playAudio();
});

function playAudio() {
    audio.src = playlist[index];
    audio.play()
    .then(_ => { /* Set up media session... */ })
    .catch(error => { console.log(error); });
}

playButton.addEventListener('pointerup', function(event) {
    playAudio();
});

يُرجى العلم أنّ معالجات إجراءات الوسائط ستظلّ موجودة. وهذا مشابه جدًا لنمط مستمع الأحداث، باستثناء أنّ معالجة حدث ما تعني أنّ المتصفّح يتوقف عن تنفيذ أي سلوك تلقائي ويستخدم ذلك كإشارة إلى أنّ تطبيق الويب يتوافق مع إجراء الوسائط. وبالتالي، لن يتم عرض عناصر التحكّم في إجراءات الوسائط ما لم يتم ضبط معالِج الإجراءات المناسب.

يُرجى العِلم أنّ إلغاء ضبط معالِج إجراء على الوسائط سهل مثل تعيينه على null.

ترجيع / تقديم

تتيح لك Media Session API عرض رمزَي إعلام الوسائط "تقديم/ترجيع" إذا كنت تريد التحكّم في مقدار الوقت الذي يتم تخطّيه.

let skipTime = 10; // Time to skip in seconds

navigator.mediaSession.setActionHandler('seekbackward', function() {
    // User clicked "Seek Backward" media notification icon.
    audio.currentTime = Math.max(audio.currentTime - skipTime, 0);
});

navigator.mediaSession.setActionHandler('seekforward', function() {
    // User clicked "Seek Forward" media notification icon.
    audio.currentTime = Math.min(audio.currentTime + skipTime, audio.duration);
});

التشغيل / الإيقاف المؤقت

يتم دائمًا عرض رمز "التشغيل/الإيقاف المؤقت" في إشعار الوسائط، ويعالج المتصفح تلقائيًا المحتوى المرتبط بالأحداث. إذا لم يعمل السلوك التلقائي لأي سبب، سيظل بإمكانك معالجة أحداث الوسائط "تشغيل" و"إيقاف مؤقت".

navigator.mediaSession.setActionHandler('play', function() {
    // User clicked "Play" media notification icon.
    // Do something more than just playing current audio...
});

navigator.mediaSession.setActionHandler('pause', function() {
    // User clicked "Pause" media notification icon.
    // Do something more than just pausing current audio...
});

الإشعارات في كل مكان

من المزايا الرائعة لواجهة برمجة التطبيقات Media Session API أنّ علبة الإشعارات ليست المكان الوحيد الذي تظهر فيه البيانات الوصفية للوسائط وعناصر التحكّم فيها. تتم مزامنة إعلام الوسائط تلقائيًا مع أي جهاز قابل للارتداء مقترن. ويظهر أيضًا على شاشات القفل.

شاشة القفل
Lock Screen - Photo by Michael Alø-Nielsen / CC BY 2.0
إشعار Wear
إشعار Wear

تشغيل المحتوى بلا إنترنت

أعلم ما الذي تفكر فيه الآن. Service worker لإنقاذ الموقف

صحيح، ولكن عليك أولاً التأكّد من وضع علامة في المربّع بجانب كل العناصر في قائمة التحقّق هذه:

  • يتم عرض كل ملفات الوسائط والأعمال الفنية باستخدام عنوان HTTP Cache-Control المناسب. سيسمح ذلك للمتصفح بتخزين موارد تم جلبها سابقًا وإعادة استخدامها. راجِع قائمة التحقّق من ذاكرة التخزين المؤقت.
  • تأكَّد من عرض جميع ملفات الوسائط والأعمال الفنية باستخدام عنوان HTTP الذي يتضمّن Allow-Control-Allow-Origin: *. سيسمح ذلك لتطبيقات الويب التابعة لجهات خارجية بالحصول على استجابات HTTP واستخدامها من خادم الويب.

استراتيجية التخزين المؤقت لخدمة Worker

في ما يتعلّق بملفات الوسائط، أنصح باستخدام استراتيجية "ذاكرة التخزين المؤقت، مع الرجوع إلى الشبكة" البسيطة كما هو موضّح في مقالة "جاك أرشيبالد".

بالنسبة إلى الأعمال الفنية، سأكون أكثر تحديدًا قليلاً وسأختار الأسلوب التالي:

  • العمل الفني If متوفّر في ذاكرة التخزين المؤقت، لذا يمكنك عرضه من ذاكرة التخزين المؤقت.
  • Else استرجاع العمل الفني من الشبكة
    • اكتمال عملية جلب If، إضافة العمل الفني للشبكة إلى ذاكرة التخزين المؤقت وعرضه
    • Else عرض العمل الفني الاحتياطي من ذاكرة التخزين المؤقت

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

const FALLBACK_ARTWORK_URL = 'fallbackArtwork.png';

addEventListener('install', event => {
    self.skipWaiting();
    event.waitUntil(initArtworkCache());
});

function initArtworkCache() {
    caches.open('artwork-cache-v1')
    .then(cache => cache.add(FALLBACK_ARTWORK_URL));
}

addEventListener('fetch', event => {
    if (/artwork-[0-9]+\.png$/.test(event.request.url)) {
    event.respondWith(handleFetchArtwork(event.request));
    }
});

function handleFetchArtwork(request) {
    // Return cache request if it's in the cache already, otherwise fetch
    // network artwork.
    return getCacheArtwork(request)
    .then(cacheResponse => cacheResponse || getNetworkArtwork(request));
}

function getCacheArtwork(request) {
    return caches.open('artwork-cache-v1')
    .then(cache => cache.match(request));
}

function getNetworkArtwork(request) {
    // Fetch network artwork.
    return fetch(request)
    .then(networkResponse => {
    if (networkResponse.status !== 200) {
        return Promise.reject('Network artwork response is not valid');
    }
    // Add artwork to the cache for later use and return network response.
    addArtworkToCache(request, networkResponse.clone())
    return networkResponse;
    })
    .catch(error => {
    // Return cached fallback artwork.
    return getCacheArtwork(new Request(FALLBACK_ARTWORK_URL))
    });
}

function addArtworkToCache(request, response) {
    return caches.open('artwork-cache-v1')
    .then(cache => cache.put(request, response));
}

السماح للمستخدم بالتحكّم في ذاكرة التخزين المؤقت

عندما يستخدِم المستخدم المحتوى من تطبيق الويب، قد تستهلك ملفات الوسائط والأعمال الفنية مساحة كبيرة على جهازه. تقع على عاتقك مسؤولية توضيح مقدار ملف التخزين المؤقت المستخدَم ومنح المستخدمين إمكانية محوه. لحسن الحظ، يمكن إجراء ذلك بسهولة باستخدام Cache API.

// Here's how I'd compute how much cache is used by artwork files...
caches.open('artwork-cache-v1')
.then(cache => cache.matchAll())
.then(responses => {
    let cacheSize = 0;
    let blobQueue = Promise.resolve();

    responses.forEach(response => {
    let responseSize = response.headers.get('content-length');
    if (responseSize) {
        // Use content-length HTTP header when possible.
        cacheSize += Number(responseSize);
    } else {
        // Otherwise, use the uncompressed blob size.
        blobQueue = blobQueue.then(_ => response.blob())
            .then(blob => { cacheSize += blob.size; blob.close(); });
    }
    });

    return blobQueue.then(_ => {
    console.log('Artwork cache is about ' + cacheSize + ' Bytes.');
    });
})
.catch(error => { console.log(error); });

// And here's how to delete some artwork files...
const artworkFilesToDelete = ['artwork1.png', 'artwork2.png', 'artwork3.png'];

caches.open('artwork-cache-v1')
.then(cache => Promise.all(artworkFilesToDelete.map(artwork => cache.delete(artwork))))
.catch(error => { console.log(error); });

ملاحظات حول التنفيذ

  • يطلب متصفّح Chrome على Android التركيز الكامل على الصوت لعرض إشعارات الوسائط فقط عندما تكون مدة ملف الوسائط 5 ثوانٍ على الأقل.
  • تتيح الأعمال الفنية للإشعارات استخدام عناوين URL للكائنات الثنائية الكبيرة (blob) وعناوين URL للبيانات.
  • إذا لم يتم تحديد عمل فني وكان هناك رمز صورة بالحجم المطلوب، ستستخدمه إعلامات الوسائط.
  • حجم العمل الفني للإشعار في Chrome لنظام Android هو 512x512. بالنسبة إلى الأجهزة المنخفضة المستوى، يكون الرمز 256x256.
  • يمكنك إغلاق إشعارات الوسائط باستخدام audio.src = ''.
  • بما أنّ Web Audio API لا تطلب ميزة "التركيز على الصوت" في Android لأسباب سابقة، فإنّ الطريقة الوحيدة لاستخدامها مع Media Session API هي ربط عنصر <audio> كمصدر إدخال لواجهة برمجة التطبيقات Web Audio API. نأمل أن تؤدي Web AudioFocus API المقترَحة إلى تحسين الوضع في القريب المدى.
  • لن تؤثّر طلبات جلسة الوسائط في إشعارات الوسائط إلا إذا كانت تأتي من الإطار نفسه الذي يتضمّن مصدر الوسائط. راجِع المقتطف أدناه.
<iframe id="iframe">
  <audio>...</audio>
</iframe>
<script>
  iframe.contentWindow.navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    ...
  });
</script>

الدعم

في وقت كتابة هذه المقالة، كان Chrome لنظام التشغيل Android هو المنصة الوحيدة التي تتيح استخدام Media Session API. يمكنك الاطّلاع على مزيد من المعلومات الحديثة عن حالة تنفيذ المتصفّح في حالة النظام الأساسي Chrome.

العيّنات والعروض التوضيحية

اطّلِع على عيّنات جلسة الوسائط الرسمية في Chrome التي تعرض Blender Foundation وأعمال Jan Morgenstern.

الموارد

مواصفات جلسة الوسائط: wicg.github.io/mediasession

المشاكل المتعلقة بالمواصفات: github.com/WICG/mediasession/issues

أخطاء Chrome: crbug.com