پخش ویدیوی وب موبایل

فرانسوا بوفور
François Beaufort

چگونه بهترین تجربه رسانه تلفن همراه را در وب ایجاد می کنید؟ آسان! همه چیز به تعامل کاربر و اهمیتی که به رسانه در یک صفحه وب می دهید بستگی دارد. فکر می‌کنم همه ما موافقیم که اگر ویدیو دلیل بازدید کاربر است، تجربه کاربر باید همه‌جانبه و دوباره جذاب باشد.

پخش ویدیوی وب موبایل

در این مقاله به شما نشان می‌دهم که چگونه به لطف تعداد زیادی از Web API، تجربه رسانه‌ای خود را به شیوه‌ای پیشرونده ارتقا دهید و آن را فراگیرتر کنید. به همین دلیل است که می‌خواهیم یک تجربه پخش موبایل ساده با کنترل‌های سفارشی، تمام صفحه و پخش پس‌زمینه بسازیم. اکنون می توانید نمونه را امتحان کنید و کد را در مخزن GitHub ما بیابید.

کنترل های سفارشی

طرح بندی HTML
شکل 1. طرح بندی HTML

همانطور که می بینید، طرح HTML ما برای پخش کننده رسانه خود بسیار ساده است: یک عنصر ریشه <div> حاوی یک عنصر رسانه <video> و یک عنصر فرزند <div> است که به کنترل های ویدئو اختصاص داده شده است.

کنترل‌های ویدیویی که بعداً به آنها خواهیم پرداخت، عبارتند از: یک دکمه پخش/مکث، یک دکمه تمام صفحه، دکمه‌های جستجو به عقب و جلو، و برخی عناصر برای زمان فعلی، مدت زمان و ردیابی زمان.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls"></div>
</div>

فراداده ویدیو را بخوانید

ابتدا صبر کنید تا ابرداده ویدیو بارگذاری شود تا مدت زمان ویدیو، زمان فعلی را تنظیم کنیم و نوار پیشرفت را مقداردهی کنیم. توجه داشته باشید که تابع secondsToTimeCode() یک تابع کاربردی سفارشی است که من نوشته‌ام که تعدادی از ثانیه‌ها را به یک رشته در قالب «hh:mm:ss» تبدیل می‌کند که در مورد ما مناسب‌تر است.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <strong>
      <div id="videoCurrentTime"></div>
      <div id="videoDuration"></div>
      <div id="videoProgressBar"></div>
    </strong>
  </div>
</div>
video.addEventListener('loadedmetadata', function () {
  videoDuration.textContent = secondsToTimeCode(video.duration);
  videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
  videoProgressBar.style.transform = `scaleX(${
    video.currentTime / video.duration
  })`;
});
فقط ابرداده ویدیویی
شکل 2. Media Player که ابرداده های ویدئویی را نشان می دهد

پخش/مکث ویدیو

اکنون که ابرداده ویدیو بارگیری شده است، اجازه دهید اولین دکمه خود را اضافه کنیم که به کاربر اجازه می دهد بسته به وضعیت پخش، ویدیو را با video.play() play و video.pause() پخش و متوقف کند.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <strong><button id="playPauseButton"></button></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
playPauseButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (video.paused) {
    video.play();
  } else {
    video.pause();
  }
});

به‌جای تنظیم کنترل‌های ویدیوی خود در شنونده رویداد click ، از play و pause رویدادهای ویدیویی استفاده می‌کنیم. ایجاد رویدادهای کنترلی ما به انعطاف‌پذیری کمک می‌کند (همانطور که بعداً با Media Session API خواهیم دید) و به ما این امکان را می‌دهد که اگر مرورگر در پخش مداخله کند، کنترل‌های خود را همگام نگه داریم. هنگامی که ویدیو شروع به پخش می کند، وضعیت دکمه را به "مکث" تغییر می دهیم و کنترل های ویدئو را مخفی می کنیم. هنگامی که ویدیو متوقف می شود، ما به سادگی حالت دکمه را به "پخش" تغییر می دهیم و کنترل های ویدئو را نشان می دهیم.

video.addEventListener('play', function () {
  playPauseButton.classList.add('playing');
});

video.addEventListener('pause', function () {
  playPauseButton.classList.remove('playing');
});

هنگامی که زمان نشان داده شده توسط ویژگی currentTime ویدیو از طریق رویداد ویدیوی timeupdate تغییر می‌کند، کنترل‌های سفارشی خود را نیز در صورت مشاهده به‌روزرسانی می‌کنیم.

video.addEventListener('timeupdate', function () {
  if (videoControls.classList.contains('visible')) {
    videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
    videoProgressBar.style.transform = `scaleX(${
      video.currentTime / video.duration
    })`;
  }
});

هنگامی که ویدیو به پایان می رسد، ما به سادگی وضعیت دکمه را به "پخش" تغییر می دهیم، currentTime ویدیو را به 0 برمی گردیم و فعلاً کنترل های ویدیو را نشان می دهیم. توجه داشته باشید که اگر کاربر نوعی ویژگی «AutoPlay» را فعال کرده باشد، می‌توانیم ویدیوی دیگری را به‌طور خودکار بارگیری کنیم.

video.addEventListener('ended', function () {
  playPauseButton.classList.remove('playing');
  video.currentTime = 0;
});

به دنبال عقب و جلو باشید

بیایید ادامه دهیم و دکمه‌های «جستجو به عقب» و «جستجوی به جلو» را اضافه کنیم تا کاربر بتواند به راحتی از برخی محتواها رد شود.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <button id="playPauseButton"></button>
    <strong
      ><button id="seekForwardButton"></button>
      <button id="seekBackwardButton"></button
    ></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
var skipTime = 10; // Time to skip in seconds

seekForwardButton.addEventListener('click', function (event) {
  event.stopPropagation();
  video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
});

seekBackwardButton.addEventListener('click', function (event) {
  event.stopPropagation();
  video.currentTime = Math.max(video.currentTime - skipTime, 0);
});

مانند قبل، به جای تنظیم سبک ویدیو در شنوندگان رویداد click این دکمه‌ها، از رویدادهای ویدیویی seeking و seeked برای تنظیم روشنایی ویدیو استفاده می‌کنیم. کلاس CSS seeking سفارشی من به سادگی filter: brightness(0); .

video.addEventListener('seeking', function () {
  video.classList.add('seeking');
});

video.addEventListener('seeked', function () {
  video.classList.remove('seeking');
});

در زیر آنچه ما تا کنون ایجاد کرده ایم آورده شده است. در بخش بعدی، دکمه تمام صفحه را اجرا می کنیم.

تمام صفحه

در اینجا ما قصد داریم از چندین وب API برای ایجاد یک تجربه تمام صفحه بی نقص و بدون درز استفاده کنیم. برای مشاهده عملکرد آن، نمونه را بررسی کنید.

بدیهی است که لازم نیست از همه آنها استفاده کنید. فقط مواردی را انتخاب کنید که برای شما منطقی هستند و آنها را برای ایجاد جریان سفارشی خود ترکیب کنید.

جلوگیری از تمام صفحه خودکار

در iOS، با شروع پخش رسانه، عناصر video به طور خودکار وارد حالت تمام صفحه می شوند. از آنجایی که می‌خواهیم تا حد امکان تجربه رسانه‌ای خود را در مرورگرهای تلفن همراه تنظیم و کنترل کنیم، توصیه می‌کنم ویژگی playsinline عنصر video به گونه‌ای تنظیم کنید که آن را مجبور به پخش درون خطی در iPhone کنید و در هنگام شروع پخش وارد حالت تمام صفحه نشود. توجه داشته باشید که این هیچ عارضه جانبی بر روی سایر مرورگرها ندارد.

<div id="videoContainer"></div>
  <video id="video" src="file.mp4"></video><strong>playsinline</strong></video>
  <div id="videoControls">...</div>
</div>

با کلیک روی دکمه تمام صفحه را تغییر دهید

اکنون که از تمام صفحه خودکار جلوگیری می کنیم، باید خودمان حالت تمام صفحه را برای ویدیو با API تمام صفحه کنترل کنیم. هنگامی که کاربر روی "دکمه تمام صفحه" کلیک می کند، اجازه دهید از حالت تمام صفحه با document.exitFullscreen() خارج شویم، اگر حالت تمام صفحه در حال حاضر توسط سند استفاده می شود. در غیر این صورت، در صورت موجود بودن، با روش requestFullscreen() درخواست تمام صفحه کنید یا در عنصر ویدیو فقط در iOS به webkitEnterFullscreen() بازگشت.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <button id="playPauseButton"></button>
    <button id="seekForwardButton"></button>
    <button id="seekBackwardButton"></button>
    <strong><button id="fullscreenButton"></button></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
fullscreenButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (document.fullscreenElement) {
    document.exitFullscreen();
  } else {
    requestFullscreenVideo();
  }
});

function requestFullscreenVideo() {
  if (videoContainer.requestFullscreen) {
    videoContainer.requestFullscreen();
  } else {
    video.webkitEnterFullscreen();
  }
}

document.addEventListener('fullscreenchange', function () {
  fullscreenButton.classList.toggle('active', document.fullscreenElement);
});

تغییر جهت صفحه نمایش تمام صفحه را تغییر دهید

همانطور که کاربر دستگاه را در حالت افقی می چرخاند، بیایید در این مورد هوشمند باشیم و به طور خودکار تمام صفحه را درخواست کنیم تا تجربه ای همه جانبه ایجاد کنیم. برای این کار، به Screen Orientation API نیاز داریم که هنوز در همه جا پشتیبانی نمی شود و در آن زمان در برخی از مرورگرها همچنان پیشوند دارد. بنابراین، این اولین پیشرفت ما خواهد بود.

این چگونه کار می کند؟ به محض اینکه متوجه تغییرات جهت صفحه شدیم، اگر پنجره مرورگر در حالت افقی است (یعنی عرض آن بیشتر از ارتفاع آن است) تمام صفحه را درخواست کنیم. اگر نه، بیایید از تمام صفحه خارج شویم. همین.

if ('orientation' in screen) {
  screen.orientation.addEventListener('change', function () {
    // Let's request fullscreen if user switches device in landscape mode.
    if (screen.orientation.type.startsWith('landscape')) {
      requestFullscreenVideo();
    } else if (document.fullscreenElement) {
      document.exitFullscreen();
    }
  });
}

قفل صفحه در افقی با کلیک دکمه

از آنجایی که ممکن است ویدیو در حالت افقی بهتر دیده شود، ممکن است بخواهیم وقتی کاربر روی دکمه تمام صفحه کلیک می کند صفحه را در حالت افقی قفل کنیم. ما می‌خواهیم Screen Orientation API را که قبلاً استفاده شده بود و برخی درخواست‌های رسانه‌ای ترکیب کنیم تا مطمئن شویم این تجربه بهترین است.

قفل کردن صفحه در حالت افقی به آسانی فراخوانی screen.orientation.lock('landscape') است. با این حال، ما باید این کار را فقط زمانی انجام دهیم که دستگاه در حالت عمودی با matchMedia('(orientation: portrait)') باشد و با matchMedia('(max-device-width: 768px)') بتوان آن را در یک دست نگه داشت. این یک تجربه عالی برای کاربران در تبلت باشد.

fullscreenButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (document.fullscreenElement) {
    document.exitFullscreen();
  } else {
    requestFullscreenVideo();
    <strong>lockScreenInLandscape();</strong>;
  }
});
function lockScreenInLandscape() {
  if (!('orientation' in screen)) {
    return;
  }
  // Let's force landscape mode only if device is in portrait mode and can be held in one hand.
  if (
    matchMedia('(orientation: portrait) and (max-device-width: 768px)').matches
  ) {
    screen.orientation.lock('landscape');
  }
}

باز کردن قفل صفحه در تغییر جهت دستگاه

ممکن است متوجه شده باشید که تجربه صفحه قفلی که ما به تازگی ایجاد کرده‌ایم، بی‌نقص نیست، زیرا وقتی صفحه قفل است، تغییرات جهت صفحه را دریافت نمی‌کنیم.

برای رفع این مشکل، در صورت وجود از Device Orientation API استفاده کنیم. این API اطلاعاتی را از سخت افزار اندازه گیری موقعیت و حرکت دستگاه در فضا ارائه می دهد: ژیروسکوپ و قطب نما دیجیتال برای جهت گیری آن و شتاب سنج برای سرعت آن. وقتی تغییر جهت دستگاه را تشخیص می‌دهیم، اگر کاربر دستگاه را در حالت عمودی نگه می‌دارد و صفحه در حالت افقی قفل است، بیایید قفل صفحه را با screen.orientation.unlock() باز کنیم.

function lockScreenInLandscape() {
  if (!('orientation' in screen)) {
    return;
  }
  // Let's force landscape mode only if device is in portrait mode and can be held in one hand.
  if (matchMedia('(orientation: portrait) and (max-device-width: 768px)').matches) {
    screen.orientation.lock('landscape')
    <strong>.then(function() {
      listenToDeviceOrientationChanges();
    })</strong>;
  }
}
function listenToDeviceOrientationChanges() {
  if (!('DeviceOrientationEvent' in window)) {
    return;
  }
  var previousDeviceOrientation, currentDeviceOrientation;
  window.addEventListener(
    'deviceorientation',
    function onDeviceOrientationChange(event) {
      // event.beta represents a front to back motion of the device and
      // event.gamma a left to right motion.
      if (Math.abs(event.gamma) > 10 || Math.abs(event.beta) < 10) {
        previousDeviceOrientation = currentDeviceOrientation;
        currentDeviceOrientation = 'landscape';
        return;
      }
      if (Math.abs(event.gamma) < 10 || Math.abs(event.beta) > 10) {
        previousDeviceOrientation = currentDeviceOrientation;
        // When device is rotated back to portrait, let's unlock screen orientation.
        if (previousDeviceOrientation == 'landscape') {
          screen.orientation.unlock();
          window.removeEventListener(
            'deviceorientation',
            onDeviceOrientationChange,
          );
        }
      }
    },
  );
}

همانطور که می بینید، این تجربه تمام صفحه یکپارچه ای است که ما به دنبال آن بودیم. برای مشاهده این در عمل، نمونه را بررسی کنید.

پخش پس زمینه

هنگامی که تشخیص دادید یک صفحه وب یا یک ویدیو در صفحه وب دیگر قابل مشاهده نیست، ممکن است بخواهید تجزیه و تحلیل خود را به روز کنید تا این موضوع را منعکس کند. این همچنین می‌تواند بر پخش فعلی تأثیر بگذارد، مثلاً در انتخاب آهنگ دیگری، توقف آن یا حتی نمایش دکمه‌های سفارشی به کاربر.

توقف موقت ویدیو در تغییر نمایان بودن صفحه

با استفاده از Page Visibility API ، می‌توانیم دید فعلی یک صفحه را تعیین کنیم و از تغییرات دید مطلع شویم. وقتی صفحه پنهان است، کد زیر ویدیو را متوقف می‌کند. زمانی که قفل صفحه فعال است یا زمانی که برگه ها را تغییر می دهید، این اتفاق می افتد.

از آنجایی که اکثر مرورگرهای تلفن همراه اکنون کنترل‌هایی را خارج از مرورگر ارائه می‌کنند که امکان ازسرگیری ویدیوی متوقف‌شده را فراهم می‌کند، توصیه می‌کنم این رفتار را فقط در صورتی تنظیم کنید که کاربر مجاز به پخش در پس‌زمینه باشد.

document.addEventListener('visibilitychange', function () {
  // Pause video when page is hidden.
  if (document.hidden) {
    video.pause();
  }
});

نمایش/پنهان کردن دکمه بی‌صدا در تغییر قابلیت مشاهده ویدیو

اگر از Intersection Observer API جدید استفاده می‌کنید، می‌توانید بدون هیچ هزینه‌ای دقیق‌تر باشید. این API به شما این امکان را می دهد که بدانید چه زمانی یک عنصر مشاهده شده وارد نمای مرورگر می شود یا از آن خارج می شود.

بیایید یک دکمه بی صدا را بر اساس قابلیت مشاهده ویدیو در صفحه نشان دهیم/پنهان کنیم. اگر ویدیو در حال پخش است اما در حال حاضر قابل مشاهده نیست، یک دکمه بی‌صدا کوچک در گوشه سمت راست پایین صفحه نشان داده می‌شود تا کنترل صدای ویدیو را به کاربر بدهد. رویداد ویدیویی volumechange برای به‌روزرسانی استایل دکمه بی‌صدا استفاده می‌شود.

<button id="muteButton"></button>
if ('IntersectionObserver' in window) {
  // Show/hide mute button based on video visibility in the page.
  function onIntersection(entries) {
    entries.forEach(function (entry) {
      muteButton.hidden = video.paused || entry.isIntersecting;
    });
  }
  var observer = new IntersectionObserver(onIntersection);
  observer.observe(video);
}

muteButton.addEventListener('click', function () {
  // Mute/unmute video on button click.
  video.muted = !video.muted;
});

video.addEventListener('volumechange', function () {
  muteButton.classList.toggle('active', video.muted);
});

هر بار فقط یک ویدیو پخش کنید

اگر بیش از یک ویدیو در یک صفحه وجود دارد، پیشنهاد می‌کنم فقط یکی را پخش کنید و بقیه را به‌طور خودکار متوقف کنید تا کاربر مجبور به شنیدن چندین آهنگ صوتی در حال پخش همزمان نباشد.

// This array should be initialized once all videos have been added.
var videos = Array.from(document.querySelectorAll('video'));

videos.forEach(function (video) {
  video.addEventListener('play', pauseOtherVideosPlaying);
});

function pauseOtherVideosPlaying(event) {
  var videosToPause = videos.filter(function (video) {
    return !video.paused && video != event.target;
  });
  // Pause all other videos currently playing.
  videosToPause.forEach(function (video) {
    video.pause();
  });
}

اعلان‌های رسانه را سفارشی کنید

با Media Session API ، می‌توانید اعلان‌های رسانه را با ارائه ابرداده برای ویدیوی در حال پخش سفارشی کنید. همچنین به شما امکان می دهد رویدادهای مرتبط با رسانه مانند جستجو یا ردیابی تغییر را که ممکن است از اعلان ها یا کلیدهای رسانه باشد، مدیریت کنید. برای مشاهده این در عمل، نمونه را بررسی کنید.

هنگامی که برنامه وب شما در حال پخش صدا یا تصویر است، از قبل می توانید یک اعلان رسانه را در سینی اعلان مشاهده کنید. در اندروید، کروم تمام تلاش خود را می کند تا با استفاده از عنوان سند و بزرگترین تصویر نمادی که می تواند پیدا کند، اطلاعات مناسب را نشان دهد.

بیایید ببینیم چگونه می‌توان این اعلان رسانه را با تنظیم برخی فراداده‌های جلسه رسانه مانند عنوان، هنرمند، نام آلبوم و اثر هنری با Media Session API سفارشی کرد.

playPauseButton.addEventListener('click', function(event) {
  event.stopPropagation();
  if (video.paused) {
    video.play()
    <strong>.then(function() {
      setMediaSession();
    });</strong>
  } else {
    video.pause();
  }
});
function setMediaSession() {
  if (!('mediaSession' in navigator)) {
    return;
  }
  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 کنونی هنگام شروع پخش مورد استفاده قرار خواهد گرفت. به همین دلیل است که باید آن را به روز کنید تا مطمئن شوید که همیشه اطلاعات مرتبط را در اعلان رسانه نشان می دهید.

اگر برنامه وب شما یک لیست پخش ارائه می دهد، ممکن است بخواهید به کاربر اجازه دهید تا مستقیماً از طریق اعلان رسانه با برخی از نمادهای "تراک قبلی" و "تراک بعدی" در لیست پخش شما پیمایش کند.

if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('previoustrack', function () {
    // User clicked "Previous Track" media notification icon.
    playPreviousVideo(); // load and play previous video
  });
  navigator.mediaSession.setActionHandler('nexttrack', function () {
    // User clicked "Next Track" media notification icon.
    playNextVideo(); // load and play next video
  });
}

توجه داشته باشید که کنترل‌کننده‌های کنش رسانه‌ای همچنان ادامه خواهند داشت. این بسیار شبیه الگوی شنونده رویداد است با این تفاوت که مدیریت یک رویداد به این معنی است که مرورگر انجام هر گونه رفتار پیش فرض را متوقف می کند و از آن به عنوان سیگنالی استفاده می کند که برنامه وب شما از عملکرد رسانه پشتیبانی می کند. بنابراین، کنترل‌های کنش رسانه‌ای نشان داده نمی‌شوند مگر اینکه کنترل‌کننده عملکرد مناسب را تنظیم کنید.

به هر حال، لغو تنظیم یک کنترل کننده اکشن رسانه به آسانی اختصاص دادن آن به null است.

Media Session API به شما این امکان را می‌دهد که نمادهای اعلان رسانه "Seek Backward" و "Seek Forward" را نشان دهید، اگر می‌خواهید مدت زمان پرش را کنترل کنید.

if ('mediaSession' in navigator) {
  let skipTime = 10; // Time to skip in seconds

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

نماد «پخش/مکث» همیشه در اعلان رسانه نشان داده می‌شود و رویدادهای مرتبط به‌طور خودکار توسط مرورگر مدیریت می‌شوند. اگر به دلایلی رفتار پیش‌فرض درست نشد، همچنان می‌توانید رویدادهای رسانه «پخش» و «مکث» را مدیریت کنید .

نکته جالب در مورد Media Session API این است که سینی اعلان تنها مکانی نیست که متادیتا و کنترل های رسانه قابل مشاهده هستند. اعلان رسانه به صورت خودکار با هر دستگاه پوشیدنی جفت شده همگام سازی می شود. و همچنین در صفحه های قفل نمایش داده می شود.

بازخورد