התאמה אישית של התראות מדיה וטיפול בפלייליסטים

François Beaufort
François Beaufort

בעזרת Media Session API החדש, אפשר עכשיו להתאים אישית את ההתראות על מדיה על ידי מתן מטא-נתונים של המדיה שמופעלת באפליקציית האינטרנט. הוא גם מאפשר לטפל באירועים שקשורים למדיה, כמו דילוג או שינוי טראק, שעשויים להגיע מהתראות או ממפתחות מדיה. מגניב, נכון? כדאי לנסות את הדוגמאות הרשמיות של Media Session.

התמיכה ב-Media Session API מתחילה ב-Chrome 57 (גרסת בטא בפברואר 2017, גרסת יציבה במרץ 2017).

אמ;לק לגבי סשן מדיה
תמונה מאת Michael Alø-Nielsen / CC BY 2.0

אני רוצה לקבל את מה שאני רוצה

כבר שמעתם על 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();
});

חשוב לזכור שמטפלי הפעולות של המדיה ימשיכו לפעול. הפתרון הזה דומה מאוד לדפוס של מאזין לאירועים, אלא שבטיפול באירוע הדפדפן מפסיק לבצע את התנהגות ברירת המחדל שלו ומשתמש בכך כאות לכך שאפליקציית האינטרנט תומכת בפעולה של המדיה. לכן, פקדי הפעולות במדיה לא יוצגו אלא אם תגדירו את ה-handler המתאים של הפעולות.

דרך אגב, ביטול ההגדרה של בורר פעולות מדיה קל כמו הקצאה שלו ל-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 הוא שחלונית ההתראות היא לא המקום היחיד שבו מוצגים אמצעי הבקרה והמטא-נתונים של המדיה. ההתראה על מדיה מסתנכרנת באופן אוטומטי עם כל מכשיר לבישה מותאם. הוא מופיע גם במסכי נעילה.

מסך הנעילה
מסך נעילה – תמונה של Michael Alø-Nielsen / CC BY 2.0
התראה ב-Wear
התראת Wear

איך להפעיל את המשחקים במצב אופליין

ברור לי מה עובר כרגע בראש שלך. קובץ שירות (service worker) ניצל את המקרה!

נכון, אבל קודם כול, כדאי לוודא שכל הפריטים ברשימת המשימות הזו מסומנים:

  • כל קובצי המדיה והגרפיקה מוצגים עם כותרת ה-HTTP המתאימה Cache-Control. כך הדפדפן יוכל לשמור במטמון משאבים שאוחזרו בעבר ולהשתמש בהם שוב. רשימת משימות בנושא שמירה במטמון
  • חשוב לוודא שכל קובצי המדיה והגרפיקה מוצגים עם כותרת ה-HTTP‏ Allow-Control-Allow-Origin: *. כך אפליקציות אינטרנט של צד שלישי יוכלו לאחזר ולצרוך תגובות HTTP משרת האינטרנט שלכם.

אסטרטגיית שמירה במטמון של Service Worker

לגבי קובצי מדיה, מומלץ להשתמש באסטרטגיה פשוטה של אחסון במטמון, חזרה לרשת, כפי שמתואר במאמר של Jake Archibald.

עם זאת, לגבי גרפיקה, הייתי ממליץ להיות קצת יותר ספציפי ולבחור בגישה הבאה:

  • הגרפיקה של 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 תשפר את המצב בעתיד הקרוב.
  • קריאות של Media Session ישפיעו על התראות מדיה רק אם הן מגיעות מאותו פריים שבו נמצא משאב המדיה. קטע הקוד שלמטה מראה איך עושים את זה.
<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.

משאבים

מפרט Media Session:‏ wicg.github.io/mediasession

בעיות במפרטים: github.com/WICG/mediasession/issues

באגים ב-Chrome:‏ crbug.com