تشغيل الفيديو على الويب على الأجهزة الجوّالة

فرانسوا بوفورت
"فرانسوا بوفورت"

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

تشغيل الفيديو على الويب على الأجهزة الجوّالة

أوضح لك في هذه المقالة كيفية تحسين تجربة الوسائط بطريقة تدريجية وجعلها أكثر شمولية بفضل عدد كبير من واجهات برمجة تطبيقات الويب. لهذا السبب، سنعمل على إنشاء تجربة بسيطة لمشغّل شبكة الجوّال من خلال عناصر تحكّم مخصّصة وإمكانية التشغيل بملء الشاشة وفي الخلفية. يمكنك تجربة النموذج الآن والعثور على الرمز في مستودع 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. مشغّل الوسائط يعرض البيانات الوصفية للفيديو

تشغيل الفيديو أو إيقافه مؤقتًا

والآن بعد تحميل البيانات الوصفية للفيديو، سنضيف الزر الأول الذي يتيح للمستخدم تشغيل الفيديو وإيقافه مؤقتًا باستخدام 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. واعتمادًا على أحداث عناصر التحكم يساعدنا في تحقيق المرونة (كما سنرى لاحقًا في واجهة برمجة التطبيقات لجلسات الوسائط) وسيتيح لنا الحفاظ على تزامن عناصر التحكم في حالة تدخل المتصفح في التشغيل. عند بدء تشغيل الفيديو، نغيّر حالة الزر إلى "إيقاف مؤقت" ونخفي عناصر التحكم في الفيديو. عندما يتوقف الفيديو مؤقتًا، نغيّر ببساطة حالة الزر إلى "تشغيل" ونعرض عناصر التحكم في الفيديو.

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() إذا كان وضع ملء الشاشة قيد الاستخدام حاليًا في المستند. في الحالات الأخرى، يمكنك طلب وضع ملء الشاشة في حاوية الفيديو باستخدام الطريقة 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 المستخدمة سابقًا وبعض طلبات الوسائط للتأكد من أنّ هذه التجربة هي الأفضل.

يمكنك قفل الشاشة في الوضع الأفقي بسهولة إجراء اتصال بـ "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,
          );
        }
      }
    },
  );
}

كما ترى، هذه هي التجربة السلسة لملء الشاشة التي كنا نبحث عنها. للاطّلاع على هذا التغيير عمليًا، يمكنك مراجعة العيّنة.

تشغيل المحتوى في الخلفية

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

إيقاف الفيديو مؤقتًا عند تغيير مستوى عرض الصفحة

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

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

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

إظهار/إخفاء زر كتم الصوت عند تغيير مستوى عرض الفيديو

إذا كنت تستخدم Intersection Monitorer 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 قصارى جهده لعرض المعلومات المناسبة باستخدام عنوان المستند وأكبر صورة للرمز يمكن أن يعثر عليها.

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

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.

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

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);
  });
}

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

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

إضافة ملاحظات