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

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

كما ترى، تنسيق 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
})`;
});

تشغيل/إيقاف تشغيل الفيديو
بعد تحميل البيانات الوصفية للفيديو، لنضيف الزر الأول الذي يتيح للمستخدمين تشغيل الفيديو وإيقافه مؤقتًا باستخدام video.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، ونعرض عناصر التحكّم في الفيديو في الوقت الحالي. يُرجى العِلم أنّه يمكننا
أيضًا اختيار تحميل فيديو آخر تلقائيًا إذا فعّل المستخدم نوعًا من
ميزة "التشغيل التلقائي".
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');
});
في ما يلي ما أنشأناه حتى الآن. في القسم التالي، سننفّذ زر وضع ملء الشاشة.
ملء الشاشة
سنستفيد هنا من العديد من واجهات برمجة التطبيقات لإنشاء تجربة مثالية وسلسة بملء الشاشة. للاطّلاع على طريقة استخدام هذه الميزة، يمكنك الاطّلاع على عيّنة.
بالطبع، ليس عليك استخدام كل هذه الأدوات. ما عليك سوى اختيار الخطوات التي تبدو منطقية بالنسبة إليك ودمجها لإنشاء مسارك المخصّص.
منع وضع ملء الشاشة التلقائي
على أجهزة 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>
تبديل وضع ملء الشاشة عند النقر على الزر
بما أنّنا نمنع الآن وضع ملء الشاشة التلقائي، علينا معالجة
وضع ملء الشاشة للفيديو بأنفسنا باستخدام Fullscreen API. عندما يُقرِّر المستخدِم
النقر على "زرّ ملء الشاشة"، لنخرج من وضع ملء الشاشة باستخدام
document.exitFullscreen()
إذا كان وضع ملء الشاشة قيد الاستخدام حاليًا في
المستند. بخلاف ذلك، يمكنك طلب عرض الفيديو في وضع ملء الشاشة على حاوية الفيديو باستخدام الطريقة
requestFullscreen()
إذا كانت متاحة، أو يمكنك استخدام العنصرwebkitEnterFullscreen()
بدلاً من ذلك في
عنصر الفيديو على نظام التشغيل iOS فقط.
<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 إذا كانت متوفرة. توفّر واجهة برمجة التطبيقات هذه معلومات من الأجهزة التي تقيس
موقع الجهاز وحرّكته في الفضاء: الجيروسكوب والبوصلة الرقمية لتحديد
الاتجاه، ومقياس التسارع لتحديد السرعة. عندما نرصد
تغييرًا في اتجاه الجهاز، لنلغِ قفل الشاشة باستخدام 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,
);
}
}
},
);
}
كما ترى، هذه هي التجربة السلسة التي أردناها في وضع ملء الشاشة. للاطّلاع على مثال عملي، يمكنك الاطّلاع على النموذج.
التشغيل في الخلفية
عندما ترصد أنّ صفحة ويب أو فيديو في صفحة الويب لم يعُد مرئيًا، ننصحك بتعديل الإحصاءات لتعكس ذلك. وقد يؤثّر ذلك أيضًا في عملية التشغيل الحالية، مثل اختيار مقطع صوتي مختلف أو إيقافه مؤقتًا أو حتى عرض buttons مخصّصة للمستخدم على سبيل المثال.
إيقاف الفيديو مؤقتًا عند تغيير مستوى رؤية الصفحة
باستخدام واجهة برمجة التطبيقات Page Visibility API، يمكننا تحديد مستوى الظهور الحالي لإحدى الصفحات والحصول على إشعارات بشأن تغييرات مستوى الظهور. يؤدي الرمز أدناه إلى إيقاف الفيديو مؤقتًا عند إخفاء الصفحة. يحدث ذلك عندما يكون قفل الشاشة مفعّلاً أو عند تبديل علامات التبويب مثلاً.
بما أنّ معظم متصفّحات الأجهزة الجوّالة تقدّم الآن عناصر تحكّم خارج المتصفّح تتيح مجددًا تشغيل فيديو تم إيقافه مؤقتًا، أنصحك بعدم ضبط هذا السلوك إلا إذا كان مسموحًا للمستخدم بالتشغيل في الخلفية.
document.addEventListener('visibilitychange', function () {
// Pause video when page is hidden.
if (document.hidden) {
video.pause();
}
});
إظهار/إخفاء زر كتم الصوت عند تغيير مستوى عرض الفيديو
إذا كنت تستخدم واجهة برمجة التطبيقات الجديدة Intersection Observer 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، يمكنك أيضًا تخصيص إعلامات الوسائط من خلال تقديم بيانات وصفية للفيديو الذي يتم تشغيله حاليًا. ويسمح لك أيضًا بمعالجة الأحداث ذات الصلة بالوسائط، مثل تغيير مسار التقديم أو الإيقاف أو التتبع، التي قد تأتي من الإشعارات أو مفاتيح الوسائط. للاطّلاع على مثال عملي، يمكنك التمرير إلى العينة.
عندما يشغّل تطبيق الويب الصوت أو الفيديو، يمكنك رؤية إشعار إعلام بالوسائط في علبة الإشعارات. على أجهزة Android، يبذل Chrome قصارى جهده لعرض المعلومات المناسبة باستخدام عنوان المستند وأكبر صورة رمز يمكنه العثور عليها.
لنطّلِع على كيفية تخصيص إشعار الوسائط هذا من خلال ضبط بعض البيانات الوصفية لجلسة الوسائط، مثل العنوان والفنان واسم الألبوم والعمل الفني باستخدام 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 عرض رمزَي الإشعارَين "تقديم/ترجيع" في الوسائط إذا كنت تريد التحكّم في مقدار الوقت الذي يتم تخطّيه.
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 أنّ علبة الإشعارات ليست المكان الوحيد الذي تظهر فيه البيانات الوصفية للوسائط وعناصر التحكّم فيها. تتم مزامنة إعلام الوسائط تلقائيًا مع أي جهاز قابل للارتداء مقترن. ويظهر أيضًا على شاشات القفل.