كيف يمكنك إنشاء أفضل تجربة وسائط على الويب للأجهزة الجوّالة؟ إنّها عملية بسيطة للغاية! يعتمد كل ذلك على تفاعل المستخدمين والأهمية التي توليها للوسائط على صفحة الويب. أعتقد أنّنا نتفق جميعًا على أنّه إذا كان الفيديو هو السبب الرئيسي لزيارة المستخدم، يجب أن تكون تجربة المستخدم غامرة وجذابة.
في هذه المقالة، سأوضّح لك كيفية تحسين تجربة الوسائط بشكل تدريجي وجعلها أكثر تفاعلاً بفضل مجموعة كبيرة من واجهات برمجة التطبيقات على الويب. لهذا السبب، سننشئ تجربة بسيطة لمشغّل الفيديو على الأجهزة الجوّالة مع عناصر تحكّم مخصّصة ووضع ملء الشاشة والتشغيل في الخلفية. يمكنك تجربة النموذج الآن والعثور على الرمز في مستودع 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>
تبديل وضع ملء الشاشة عند النقر على الزر
بعد أن منعنا وضع ملء الشاشة التلقائي، علينا التعامل مع وضع ملء الشاشة للفيديو باستخدام واجهة برمجة التطبيقات لملء الشاشة. عندما ينقر المستخدم على "زر ملء الشاشة"، لنخرج من وضع ملء الشاشة باستخدام document.exitFullscreen() إذا كان وضع ملء الشاشة قيد الاستخدام حاليًا في المستند. Otherwise, request fullscreen on the video container with the method
requestFullscreen() if available or fallback to webkitEnterFullscreen() on
the video element only on 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,
);
}
}
},
);
}
كما ترون، هذه هي تجربة ملء الشاشة السلسة التي كنا نبحث عنها. للاطّلاع على مثال واقعي، يمكنك مراجعة النموذج.
التشغيل في الخلفية
عندما تكتشف أنّ صفحة ويب أو فيديو في صفحة الويب لم يعودا مرئيَّين، قد تحتاج إلى تعديل إحصاءاتك لتعكس ذلك. قد يؤثر ذلك أيضًا في التشغيل الحالي، مثل اختيار مقطع صوتي مختلف أو إيقافه مؤقتًا أو حتى عرض أزرار مخصّصة للمستخدم على سبيل المثال.
إيقاف الفيديو مؤقتًا عند تغيير مستوى ظهور الصفحة
باستخدام 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 أنّ لوحة الإشعارات ليست المكان الوحيد الذي تظهر فيه البيانات الوصفية وعناصر التحكّم في الوسائط. تتم مزامنة إشعار الوسائط تلقائيًا مع أي جهاز مقترن قابل للارتداء. ويظهر أيضًا على شاشات القفل.