يستغرق استهلاك الوسائط جزءًا كبيرًا من الوقت الذي يقضيه المستخدمون على الأجهزة الجوّالة، ما يتطلّب أن تكون تجربة التشغيل خالية من العيوب وفعّالة للغاية وتراعي عمر البطارية. بالإضافة إلى ذلك، يتوقّع المستخدمون أن تنتقل هذه التجارب بسلاسة على جميع أجهزتهم، من الهواتف إلى أجهزة التلفزيون في غرف المعيشة وغيرها.
تعرَّف على كيفية الاستفادة من التحديثات في بنية Media3 وExoPlayer لتشغيل الوسائط بسلاسة على مختلف أشكال الأجهزة. تتمحور المناقشة حول إنشاء واجهات متجاوبة باستخدام حزمة أدوات واجهة المستخدم Media3 Compose، والتخلص من التخزين المؤقت في الخلاصات باستخدام PreloadManager الجديد، وتوسيع نطاق تطبيقك ليشمل الشاشات الكبيرة وAndroid XR باستخدام CastPlayer والصوت المكاني.
النص
حسنًا
مرحبًا بالجميع.
اسمي توني.
أنا مهندس في فريق Android Media.
وسنتحدث مع "كريستينا" و"دونوفان" عن كيفية الاستفادة من Jetpack Media3 في تلبية جميع احتياجات الوسائط على Android.
Media3 هي مكتبة دعم Android لكل ما يتعلّق بالوسائط، سواء كان ذلك تشغيل الوسائط باستخدام ExoPlayer، أو مشاركة الوسائط من خلال جلسة الوسائط وCast، أو تعديل الوسائط باستخدام Transformer، أو فحص الوسائط، مثل استخراج اللقطات أو البيانات الوصفية.
تغطي Media3 هذه المساحة بأكملها في مجموعة مكتبات واحدة.
وبما أنّها مكتبة دعم، يتم تجميعها في تطبيقك.
وهذا يعني أنّه يمكننا ضمان الاتساق، وبالتالي تشغيل الرمز نفسه على جميع إصدارات نظام التشغيل وجميع الأجهزة.
ويساعد أيضًا في تحسين قابلية التحديث.
ويتم تطبيق كل إصلاح للأخطاء نُجريه على المستوى 24 من واجهة برمجة التطبيقات، ولا يرتبط بأي دورة إصدار لنظام التشغيل.
وبصفتها مكتبة دعم، يمكننا السماح بخيارات تخصيص غنية جدًا، وهو ما لن يكون ممكنًا في إطار عمل Android.
وبما أنّها مكتبة على GitHub، فإنّنا نركّز على المصادر المفتوحة أولاً.
هذا يعني أن كل تغيير نقوم به متاح على الفور على جيت هب.
لدينا أداة تتبُّع مشاكل نشطة، ونقبل طلبات السحب.
الأمر الرئيسي الذي يجب تسليط الضوء عليه هنا، في جميع حالات الاستخدام هذه، هو أنّنا نهتم حقًا بجميع مستويات التفاصيل، بدءًا من عناصر في واجهة المستخدم العالية المستوى وحتى واجهات برمجة التطبيقات البسيطة الجاهزة للاستخدام، بالإضافة إلى خيارات التخصيص العميقة، والدعم الواسع لمختلف التنسيقات والمعايير، والأداء والكفاءة التي تدعم كل ذلك.
تغطي Media3 هذه المساحة بأكملها.
واليوم، سنحاول التعمّق في بعض هذه المواضيع.
سنتحدث عن وحدة واجهة المستخدم الجديدة في Compose، ومشغّل CastPlayer الجديد الذي يسهّل المشاركة مع جلسة الوسائط، ومسار التعديل المُجدَّد الذي يتضمّن تأثيرات متقدّمة جديدة، وأدوات جديدة لاسترداد البيانات الوصفية والإطارات، وبعض تكنولوجيات البث الجديدة، لا سيما فيديو وصوت Eclipsa، وعدد من أدوات جودة التجربة للتحميل المُسبَق والتقديم والترجيع وتوفير شحن البطارية.
لننتقل الآن مباشرةً إلى الموضوع الأول، وهو كيف يمكن أن تساعد Media3 في إنشاء عناصر في واجهة المستخدم للوسائط.
لقد نشرنا مؤخرًا وحدتَين جديدتَين: Media3 UI Compose وMedia3 UI Compose Material 3.
هنا، يمكنك الاطّلاع على دالة مركّبة لمشغّل وسائط عالي المستوى، كما تم إطلاقها في الإصدار 1.10 من Media3 بدون أي تعديلات إضافية.
@Composable
fun videoScreen() {
Player(viewModel.exoPlayer)
}ويمكن استخدامه على الفور.
ويحتاج فقط إلى مشغّل Media3.
في المثال هنا، هو ExoPlayer، والذي عادةً ما يكون لديك في نموذج العرض الخاص بك.
ولكن يمكن أن يكون شيئًا آخر، مثل CastPlayer أو وحدة تحكّم في الوسائط أو أي مشغّل آخر لديك في تطبيقك.
تجدر الإشارة إلى أنّ ExoPlayer هو المصدر الموثوق هنا.
يحتوي على جميع المعلومات، وتهتم الدالة المركّبة للمشغّل بعرض واجهة المستخدم وربط كل شيء.
ولكن ماذا لو أردت تخصيص واجهة المستخدم هذه، أو إضافة أزرار جديدة، أو تغيير المواضع، أو الأنماط، أو إضافة نصوص فوقية؟
تتكوّن الدالة المركّبة للمشغّل من وحدات أساسية.
ويمكنك استبدالها.
يمكنك تخصيص كل كتلة فردية، أو يمكنك استخدامها بشكل مستقل في مكان آخر في تطبيقك.
على سبيل المثال، يمكنك تخصيص الجزء العلوي والوسط وbottomControls بشكل منفصل.
يمكنك أيضًا تخصيص مظهر الشاشة التي تظهر عندما لا يتم تشغيل الفيديوهات أو تراكب الخطأ.
ولكن هذه ليست سوى الطبقة الأولى.
يحتوي المركز التلقائي و"bottomControls" أيضًا على خانات أكثر تفصيلاً يمكنك تخصيصها.
على سبيل المثال، يمكنك تبديل الزر المركزي مع ترك كل شيء آخر كما هو، أو إضافة عناصر أعلى شريط التمرير أو أسفله.
توفّر Media3 Compose جميع العناصر الأساسية التي تحتاج إليها لإجراء ذلك.
على سبيل المثال، لدينا عدد من الأزرار، مثل زر التشغيل/الإيقاف المؤقت، ووضع التكرار، ووضع الترتيب العشوائي، وزر كتم الميكروفون، وغيرها.
بعد ذلك، هناك مؤشرات التقدم، مثل شريط التمرير ونص الموضع المعروض على الشاشة.
هناك عدد من الحاويات والتراكبات أيضًا.
الدالة المركّبة العالية المستوى التي عرضتها لكم هي مثال واحد، ولكن هناك المزيد من الأمثلة، مثل تراكبات الأخطاء أو الترجمات وغيرها الكثير.
بالإضافة إلى ذلك، لدينا مكوّنات مفيدة أخرى، مثل وحدة التحكّم المصغّرة هذه.
يمكنك وضعها في بطاقة سفلية أثناء تنقّل المستخدم في تطبيقك.
ولم ننتهِ بعد.
نعمل حاليًا على توسيع هذه المكتبة بشكل أكبر، على سبيل المثال، من خلال إضافة عناصر تحكّم جديدة في التعديل، مثل شريط تمرير الاقتصاص الذي سيتوفّر قريبًا.
والآن، لنطّلِع على عملية التخصيص هذه بشكل عملي لمعرفة كيفية استخدام هذه الوحدات الأساسية.
Player(viewModel.exoPlayer)لنفترض أنّنا نريد نقل نص الموضع والمدة، الموجود حاليًا بجانب شريط التمرير، إلى أعلاه.
Player(
player = viewModel.exoPlayer,
bottomControls = {
PlayerDefaults.BottomControls(
player,
left = {}, right = {},
above = {
PositionAndDurationText(
player, separator = " • ")
},
)
},
)لذا، ما يمكننا فعله هو إلغاء هذه bottomControls.
لا يزال بإمكاننا استخدام المكوّن التلقائي، ولكننا نضبط الخانة اليسرى واليمنى على "فارغة" لإخفائهما.
وبدلاً من ذلك، نضع نص الموضع والمدة فوقه في الخانة أعلاه مع فاصل مخصص لجعله يبدو أكثر إثارة للاهتمام.
والآن، لنفترض أنّنا نريد إزالة تراكب centerControls بالكامل، وبدلاً من ذلك، وضع زر تشغيل فوق شريط التمرير مباشرةً مع عنصر تحكّم في السرعة وزر كتم الصوت.
Player(
player = viewModel.exoPlayer,
centerControls = {},
bottomControls = { ...
above = {
Row(Modifier.fillMaxWidth()) {
PositionAndDurationText(...)
Spacer(Modifier.weight(1f))
PlaybackSpeedToggleButton(player)
MuteButton(player)
PlayPauseButton(player)
}
},
)
}وهذه هي الفكرة نفسها مرة أخرى.
يمكننا ضبط centerControls على "فارغ" لإخفائه، ويمكننا توسيع المساحة فوق شريط التمرير ببعض المساحة الإضافية والأزرار الثلاثة التي نريد عرضها.
يُرجى العِلم أنّ هذه الأزرار من المكتبة نفسها، لذا ليس عليك وضع أي رمز إضافي فيها.
هذا كل ما تحتاج إليه.
ويمكنك مواصلة السير في هذا المسار لتصميم هذه العناصر أيضًا.
إذا أردت تغيير رمز زر كتم الصوت، على سبيل المثال، ولكن مع الاحتفاظ بوظيفته، فإنه يعمل بنفس الطريقة أيضًا.
Player(
...
bottomControls = { ...
above = {
...
MuteButton(player,
painter = { painterResource(
if (showMuted)
R.drawable.muted
else
R.drawable.unmuted )})
}
},
)كما ترون هنا، يمكنكم تحديد رسام مخصّص للزر واختيار الرموز بأنفسكم.
يوفّر لك المكوّن بعض البيانات الوصفية، لذا كل ما عليك فعله هو الاطّلاع على الحالة، مثل showMuted في هذا المثال، لاختيار الرمز المناسب.
وهذه هي الميزة الرئيسية لـ Media3 Compose.
يمكنك التركيز على مظهر تطبيقك وأسلوبه، وستتولى Media3 الباقي.
فهي تقرأ الحالة من المشغّل، وتتفاعل معه، وتتعامل مع جميع عمليات الانتقال والرسوم المتحركة بينهما.
لننتقل الآن من واجهة المستخدم إلى مشاركة الوسائط.
يمكنك مشاركة حالة التشغيل مع تطبيقات أخرى على الجهاز نفسه أو مع أجهزة بعيدة متصلة.
الطريقة الأساسية لنشر المحتوى الذي يتم تشغيله حاليًا هي من خلال MediaSession.
ويتيح للآخرين الاطّلاع على حالة التشغيل والبيانات الوصفية والتحكّم في التشغيل.
هذه هي الطريقة التي يتواصل بها Gemini أو إشعارات الوسائط في واجهة مستخدم النظام أو Cast أو Wear OS أو التطبيقات المصاحبة لـ Android Auto مع تطبيقك.
MediaSession يلتف حول مشغّل Media3، ومن السهل جدًا إنشاء مشغّل بمجرد أن يكون لديك مشغّل.
MediaSession.Builder(context, player).build()مرة أخرى، يمكن أن يكون هذا المشغّل ExoPlayer أو أي مشغّل آخر في تطبيقك، تمامًا كما هو الحال مع واجهة مستخدم Compose.
إذا كنت تشغّل الوسائط في الخلفية، يمكنك استضافة ذلك عادةً داخل MediaSessionService أو MediaLibraryService، ما يعالج تلقائيًا جميع مراحل دورة الحياة، بالإضافة إلى أمور مثل الإشعارات.
class PlaybackService : MediaSessionService() {
private lateinit var mediaSession:
MediaSession
override fun onGetSession(
info: ControllerInfo) : MediaSession?
= mediaSession
}إذا كان لديك تطبيق وسائط، من المحتمل أنّك تعرف كل هذه المعلومات.
الميزة الجديدة الرائعة التي أريد أن أطلعك عليها هي CastPlayer الذي تمت إعادة كتابته مؤخرًا.
تتيح لك ميزة Google Cast نقل المحتوى الذي يتم تشغيله على جهازك المحلي إلى جهاز بعيد، مثل أجهزة Android TV أو Chromecast أو مكبّرات الصوت الذكية.
كنا ننشر حزمة Cast SDK منفصلة، وهي الآن في وضع الصيانة.
بدلاً من ذلك، يحلّ Media3 Cast محلّها بالكامل لأنّه يوفّر عملية دمج أبسط وأكثر سلاسة.
العنصر الأكثر أهمية هنا هو CastPlayer، الذي ينفّذ واجهة المشغّل العادية نفسها مثل ExoPlayer.
بينما يُستخدم ExoPlayer للتشغيل على الجهاز، فإنّ CastPlayer هو مثيل تفويض يمكنه التشغيل على الجهاز من خلال ExoPlayer أو عن بُعد من خلال بروتوكول Cast.
وهو يتولى كل شيء.
فهي ترصد التغييرات في أجهزة الإخراج، وتنشئ الاتصال، وتزامن الحالة، وتنقل عملية التشغيل عند الحاجة.
وبسبب ذلك، فإن دمج CastPlayer في تطبيقك بسيط للغاية.
val player = ExoPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()بدلاً من إنشاء ExoPlayer ومشاركته مع MediaSession بهذه الطريقة، يمكنك استخدام CastPlayer بدلاً من ذلك، وهذا كل شيء.
Cast.getSingletonInstance(context).initialize()
val player = CastPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()الشيء الآخر الوحيد الذي عليك فعله هو إعداد نظام Cast قبل استخدامه في مكان ما في تطبيقك.
يُرجى العِلم أنّ CastPlayer ينشئ ExoPlayer داخليًا للتشغيل على الجهاز، لذا لتخصيص المشغّل على الجهاز بشكل أكبر، يمكنك أيضًا إنشاء ExoPlayer خاص بك وتمريره كمشغّل على الجهاز إلى CastPlayer هذا.
Cast.getSingletonInstance(context).initialize()
val exoPlayer = ExoPlayer.Builder(context).build()
val player = CastPlayer.Builder(context)
.setLocalPlayer(exoPlayer).build()
mediaSession = MediaSession.Builder(context,player)
.build()وهذا هو مبدأ Media3 نفسه مرة أخرى.
نوفّر لك اللبنات الأساسية البسيطة والجاهزة لواجهة برمجة التطبيقات، بالإضافة إلى جميع خيارات التخصيص التي تحتاج إليها لتخصيصها بشكل أكبر.
قد تتساءل عن كيفية تبديل المستخدم بين التشغيل المحلي والتشغيل عن بُعد في هذا الإعداد.
وهي تعمل تلقائيًا مع مربّع حوار "مبدّل مخرجات النظام" في إشعار الوسائط.
هو الزر الصغير في الزاوية.
ولكن ماذا لو أردت عرض زر البث في شاشة التشغيل لتسهيل العثور عليه؟
هل تتذكرون واجهة مستخدم Compose المخصّصة التي أنشأناها قبل بضع دقائق؟
Player(
exoPlayer,
centerControls = {}
bottomControls = { /* customized */ },
)توفّر Media3 Cast أيضًا زرًا جاهزًا للاستخدام يمكنك وضعه هناك.
الأمر بسيط، فكل ما عليك فعله هو إضافة MediaRouteButton الجاهز إلى topControls، على سبيل المثال، ولا نحتاج إلى أي معلَمات أخرى لإعدادها، هذا كل ما في الأمر.
Player(
castPlayer,
centerControls = {}
bottomControls = { /* customized */ },
topControls = {
PlayerDefaults.TopControls(castPlayer) {
MediaRouteButton(
Modifier.align(Alignment.TopEnd))
}
},
)ويؤدي النقر على هذا الزر إلى فتح مربّع حوار مبدّل إخراج النظام نفسه، ثم ينقل CastPlayer التشغيل نيابةً عنك.
لاحظ أيضًا أنني ربطت CastPlayer بواجهة المستخدم هنا للتأكد من أن أي شيء يحدث عن بُعد ينعكس في واجهة المستخدم المحلية.
والآن، سأترك الكلمة لـ "كريستينا" لتتحدّث عن حالات استخدام التعديل.
شكرًا لك.
مرحبًا، لننتقل الآن إلى التعديل والمؤثرات المتقدّمة.
تتكوّن واجهات برمجة تطبيقات Media3 من مكوّنات متعدّدة.
أولاً، Transformer هي نقطة الدخول الرئيسية لواجهات برمجة التطبيقات الخاصة بالتعديل.
تعالج Transformer الوسائط وتحوّل ترميزها وتصدّرها إلى ملف.
بعد ذلك، يأتي CompositionPlayer، الذي يعرض معاينات في الوقت الفعلي لتركيبات الوسائط.
يتيح لك ذلك معاينة تعديلاتك وتحسينها قبل التصدير النهائي.
بعد ذلك، لدينا التأثيرات.
ويتيح لك التحكّم الكامل في معالجة كل إطار على حدة عند تطبيق تأثيرات مرئية بسيطة أو إضافة عناصر مركّبة للرسومات.
أخيرًا، لدينا Muxer.
وهي بمثابة مرحلة الإخراج النهائية في مسار المعالجة، حيث تأخذ المقاطع الصوتية ومقاطع الفيديو والبيانات الوصفية التي تمت معالجتها، وتكتبها في حاويات MP4 عادية.
لقد قضينا العام الماضي في إعادة التفكير في مسار تعديل الفيديوهات وإعادة إنشائه من الصفر، واليوم، يسرّني مشاركة التحسينات الرئيسية.
لماذا أعدنا إنشاء المسار؟
لذلك، واجهتنا عدة تحديات في البنية القديمة.
أولاً، لم يكن بإمكانها نشر البيانات الوصفية في جميع خطوات المعالجة.
وكانت مرتبطة ارتباطًا وثيقًا بـ OpenGL، ما حدّ من مرونة أدوات الرسومات الحديثة.
وكانت واجهة برمجة التطبيقات المعقّدة تجعل عملية التخصيص والصيانة عقبة كبيرة أمام المطوّرين.
ولمواجهة هذه التحديات، أنشأنا مسار معالجة جديدًا تمامًا.
أولاً، أصبح تجميع التشكيلات المعقّدة التي تضم عدّة مواد عرض أسهل بكثير الآن.
ثانيًا، من خلال الفصل عن OpenGL، لديك الحرية الكاملة في استخدام أُطر عمل رسومية حديثة، مثل WebGPU، على سبيل المثال.
وأخيرًا، تدير Media3 الآن برامج فك الترميز والترميز مباشرةً، ما يضمن اتساقًا وكفاءة أكبر على جميع الأجهزة.
لنلقِ نظرة على طريقة عمل هذا المسار.
تُخرج برامج فك الترميز الإطارات مباشرةً إلى مخازن الأجهزة المؤقتة التي تديرها Media3.
ينسّق Frame Aggregator اللقطات في تسلسلات متعددة، ما يمنح معالج اللقطات المخصّص إمكانية الوصول المباشر لقراءة هذه المخازن المؤقتة ومعالجتها قبل إرسالها إلى برنامج الترميز.
تُعدّ واجهة FrameProcessor نقطة الإضافة الأساسية للمعالجة على مستوى التطبيق.
public interface FrameProcessor
extends AutoCloseable {
interface Listener {
void onWakeup();
void onError(
VideoFrameProcessingException exception);
void onFrameProcessed(
Frame frame,
@Nullable SyncFenceWrapper onCompleteFence);
}
interface Factory {
FrameProcessor create(
FrameWriter output,
Executor listenerExecutor,
Listener listener);
}
boolean queue(List<AsyncFrame> frames);
void signalEndOfStream();
}من خلال واجهة المستمع، يرسل تطبيقك إشعارًا إلى Media3 عند معالجة أي إطار، مع تمرير حواجز المزامنة، لكي يعرف Media3 بدقة متى يمكن إعادة استخدام المخازن المؤقتة بأمان.
ملاحظة حول الذاكرة الموحّدة: من خلال السماح لـ Media3 بالتحكّم في مخازن الأجهزة المؤقتة، يمكن لتطبيقك قراءة البيانات وتعديلها مباشرةً في مكانها.
يقلّل هذا النهج من استهلاك الذاكرة ويحافظ على سرعة المعالجة الفائقة.
Adobe هي إحدى الشركات المطوّرة التي تستفيد من واجهات برمجة التطبيقات الجديدة للتعديل.
استخدم فريق Adobe Premiere واجهات برمجة التطبيقات لتعديل الوسائط في Media3 كأساس للبنية الأساسية لتعديل الوسائط لضمان توافق أكبر مع منظومة Android المتكاملة، مع الحفاظ على المرونة التي يحتاجون إليها لاستخدام مسار المعالجة المخصّص وWebGPU.
بعد ذلك، يسرّني الإعلان عن طرح تأثيرات جديدة كجزء من إصدار Media3 1.12.
لنبدأ بتحسينات الفيديو.
تعتمد بنيتنا على الإعدادات المعيارية.
يتكامل تطبيقك مع تأثيرات Media3 في "خدمات Google Play"، والتي تتفاعل مع وحدة تحسين "خدمات Google Play".
تتضمّن وحدة "تحسين الفيديو" إمكانات "تخطيط الدرجات اللونية" و"إزالة التشويش" و"إزالة التمويه" و"دقة سوبر".
يتطلّب دمج ميزة "تحسين الفيديو" في مسار المعالجة بضعة أسطر من الرموز البرمجية فقط.
val frameProcessorFactory = EnhancementSessionDecorator.Builder(/* context = */ this, baseGlFactory)
.setTonemappingEnabled(true)
.setDeblurAndDenoiseEnabled(true)
.setUpscaleVideoEnabled(true)
.build()
val transformerBuilder = Transformer.Builder(/* context = */ this)
.setFrameProcessorFactory(frameProcessorFactory)باستخدام EnhancementSessionDecorator.Builder، يمكنك تفعيل إمكانات محدّدة، مثل تعيين الدرجات اللونية أو إزالة التمويه أو زيادة الدقة، وتمرير frameProcessorFactory الناتج ومساعدي المخزن المؤقت للأجهزة إلى Transformer.Builder.
من المؤثرات الرائعة الأخرى التي ستتوفّر في الإصدار 1.12 من Media3 هو "صوت الاستوديو".
تحلّل ميزة "صوت الاستوديو" تلقائيًا المقطع الصوتي للفيديو وتفصله إلى أجزاء فردية، مثل الكلام والضوضاء المحيطة والموسيقى.
يمكن لصنّاع المحتوى بسهولة خفض صوت الرياح أو الضوضاء الصادرة عن الحشود مع إبراز الحوار والموسيقى بوضوح.
لدمج ميزة "تحسين الصوت"، يجب تمرير معالج الصوت الخاص بها داخل عنصر تأثير.
val editedMediaItem =
EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
.setEffects(
Effects(
listOf(
soundSeparationAudioProcessor
),
emptyList(), // video effects
)
)
.build()يؤدي ذلك إلى إرفاق معالج الصوت بملف الوسائط.
عند إنشاء معالج الصوت "استوديو الصوت"، أرفِق دالة الاستدعاء الخاصة بالمستمع لتلقّي عملية ربط القنوات الصوتية التي تم تحليلها من خلال onResult.
val soundSeparationAudioProcessor =
SoundSeparationAudioProcessor(
getApplication(),
mainExecutor,
object : SoundSeparationAudioProcessor.Listener {
override fun onResult(channelToAudioType: SparseIntArray) {
for (i in 0 until channelToAudioType.size()) {
val channelIndex = channelToAudioType.keyAt(i)
val audioType = channelToAudioType.valueAt(i)
val label = toAudioTypeName(audioType)
detectedChannelVolumes.add(ChannelVolume(channelIndex, audioType, label))
}
}
},
)يمكنك تكرار العملية في كل قناة لاستخراج فهرس القناة ونوع الصوت الذي تم التعرّف عليه والتصنيف القابل للقراءة عندما تكون نتيجة العملية متاحة.
لنستمع الآن إلى ميزة "مُحسِّن الصوت الذكي" أثناء عملها.
لذا، التأثير الأول. حبيبي، أرجوك لا تبكِ.
نحن نقطّع اللحم لعشائك.
يمكنك سماع بكاء الطفل في الخلفية، والآن مع تطبيق التأثير.
كما سمعتم، لم يكن هناك صوت بكاء طفل في الخلفية بفضل تأثير "صوت الاستوديو".
لنتعرّف الآن على كيفية استرداد البيانات الوصفية.
سنلقي نظرة على كيفية تسهيل Media3 لقراءة البيانات الوصفية للحاوية واستخراج الإطارات.
بدءًا من الإصدار MetadataRetriever، يتكامل مع الكوروتينات في Kotlin بسلاسة للحصول على رمز برمجي نظيف وغير محظور.
ويتميّز بتصميم أوضح وأقل عرضة للأخطاء باستخدام أداة استرداد واحدة غير قابلة للتغيير لكل ملف وسائط.
وتعمل بشكل متّسق على جميع إصدارات واجهة برمجة التطبيقات والأجهزة.
وهو يتيح استخدام مجموعة واسعة من تنسيقات الوسائط بشكل تلقائي.
إليك مثال عملي باستخدام MetadataRetriever.
MetadataRetriever.Builder(context, mediaItem).build().use { retriever ->
// 1. Duration (in microseconds)
val durationUs = retriever.retrieveDurationUs().await()
// 2. Track formats & container metadata (reuses the same preparation pass)
val trackGroups = retriever.retrieveTrackGroups().await()
for (i in 0 until trackGroups.length) {
val format = trackGroups[i].getFormat(0)
// Video/Audio properties: format.width, format.height, format.colorInfo …
// Rich metadata: SlowMotionData, MotionPhotoMetadata, Mp4TimestampData …
val metadata = format.metadata
}
// 3. Stream structure & manifest info
val timeline = retriever.retrieveTimeline().await()
}باستخدام وظائف Kotlin coroutines، يمكنك استرداد المدة بشكل غير متزامن وفحص تنسيقات المقاطع الصوتية والبيانات الوصفية، كل ذلك ضمن مجموعة واحدة واضحة.
لنتحدّث الآن عن FrameExtractor.
ويتيح سحب إطارات الفيديو التي تم فك ترميزها من ملف الوسائط بسرعة وكفاءة.
فهي تدير الموارد الأساسية مثل برامج فك الترميز تلقائيًا، وتتيح لك تطبيق التأثيرات المرئية أو ربط درجات الألوان في النطاق العالي الديناميكية (HDR)، وتمنحك إمكانية التحكّم الدقيق في البحث لتتمكّن من الحصول على الإطار الذي تريده بالضبط.
في ما يلي كيفية ظهور عملية استخراج الإطارات في الرمز البرمجي.
FrameExtractor.Builder(context, mediaItem)
.setSeekParameters(SeekParameters.CLOSEST_SYNC) // Fast keyframe seeking
.setEffects(listOf(Presentation.createForHeight(360))) // GPU downscaling
.setExtractHdrFrames(true) // Optional: HLG HDR or UltraHDR
.build()
.use { extractor ->
// Representative thumbnail
val thumbnail = extractor.getThumbnail().await()
// Frame at specific timestamp (bitmap + actual presentationTimeMs)
val frame = extractor.getFrame(positionMs = 5_000L).await()
}يمكنك إنشاء مثيل باستخدام خيارات مثل مَعلمات البحث، وخفض دقة وحدة معالجة الرسومات، والتأثيرات، ودعم النطاق العالي الديناميكية (HDR)، ثم انتظار الصور المصغّرة والإطارات المحدّدة بشكل غير متزامن باستخدام طلبات تعليق واضحة.
والآن، لننتقل إلى "دونوفان".
شكرًا لك، كريستينا.
لننتقل الآن إلى الحديث عن بعض تقنيات البث الأساسية التي أضفناها في Media3، بما في ذلك التوافق مع تنسيقات مثل Eclipsa Video وEclipsa Audio.
لذلك، يتمثّل أحد الجهود الأساسية في Media3 في مواكبة معايير البث، لكي لا تضطر إلى إعادة كتابة مسار الوسائط في كل مرة يتم فيها طرح مواصفات جديدة.
في ما يلي بعض الأمثلة على الميزات التي طرحناها مؤخرًا.
نستخدم ميزة توجيه المحتوى التي تتيح التبديل ديناميكيًا بين مسارات شبكة توصيل المحتوى (CDN) في الخلفية أثناء البث.
نوفّر ميزة "إدراج الإعلانات بتوجيه من الخادم" لدمج الإعلانات المخصّصة بسلاسة.
أتحنا أيضًا استخدام ترميز AV1 في نقاط دخول متعددة، بما في ذلك استخدام ترميز AV1 مع تقنية النطاق العالي الديناميكية (HDR).
نوفّر ميزة الفيديو الاحترافي على Android لالتقاط الفيديوهات وتعديلها بدقة عالية.
سنضيف أيضًا إمكانية استخدام الصور المتحركة بتنسيقَي JPEG وHEIC.
وبالطبع، سنضيف دعمًا لـ Eclipsa Video وEclipsa Audio، وسأتحدث عنهما بمزيد من التفصيل قريبًا.
والخبر السار هو أنّ كل هذه الميزات ستتوفّر تلقائيًا مع طرح الأجهزة المتوافقة.
لنلقِ الآن نظرة على المرحلة التالية من تطوّر تقنية HDR، وهي Eclipsa Video، والمشكلة الأساسية التي تحاول حلّها.
كانت تنسيقات HDR السابقة توفّر بيانات وصفية تحدّد شكل المشهد، ولكنّها لم تكن تخبر الشاشة بكيفية عرضه.
لذلك دعونا نؤسس هذا أولاً.
تختلف الإعدادات المقترَحة لمتوسط سطوع الشاشة باختلاف البيئات، وذلك لتتمكّن من عرض المحتوى بوضوح وراحة.
في غرفة ذات إضاءة خافتة، سيتراوح هذا النطاق بين 100 و300 شمعة في المتر المربع.
وعند الانتقال إلى بيئات أكثر سطوعًا، يجب أن تزيد الشاشة من سطوعها لتتمكّن من رؤية المحتوى بوضوح، إلى أن تصل إلى عرض الهاتف في ضوء الشمس المباشر، وقد تحتاج الشاشة إلى الوصول إلى 1,000 أو 2,000 أو حتى أكثر من شمعة في المتر المربع لتتمكّن من عرض المحتوى.
يعمل ممثل تطوير المبيعات التقليدي في نطاق يتراوح بين 100 و300.
وبالطبع، لا تهدف تكنولوجيا النطاق العالي الديناميكية إلى زيادة سطوع الشاشة بشكل عام.
تريد توسيع النطاق الديناميكي، لكي تظهر لك تفاصيل ساطعة في مناطق محدّدة، مثل النار وأشعة الشمس.
ومع ذلك، يضيف الهاتف الذكي بعض التعقيدات إلى المزيج، لأننا نأخذ هذه الهواتف معنا في كل مكان، إلى غرفة مظلمة في منزلنا، أو إلى شاطئ مشمس.
بالإضافة إلى ذلك، تدمج العديد من التطبيقات عناصر في واجهة المستخدم بتقنية SDR مع محتوى SDR ومحتوى HDR في خلاصات مختلطة، ما قد يؤدي إلى ظهور تأثير مزعج ومبهر أحيانًا، خاصةً عند التصفّح في وقت متأخر من الليل.
ويمكننا أن نرى سبب أهمية تقنية النطاق العالي الديناميكية، أليس كذلك؟
فهو يجعل المحتوى الخاص بنا يبدو رائعًا في المزيد من البيئات.
وقد لاحظنا أيضًا زيادة في تفاعل المستخدمين عند تفعيل ميزة HDR.
لكن المشكلة تكمن في أنّ تنسيقات HDR الحالية لا تأخذ هذه البيئات في الاعتبار.
وهنا تبرز أهمية Eclipsa Video.
توقَّف قليلاً لضحك الجمهور.
شكرًا لك.
شكرًا لك.
لذلك، بدلاً من البيانات الوصفية، يوفّر Eclipsa Video بيانات وصفية توجيهية.
ويستند ذلك إلى ثلاث ركائز أساسية.
الركيزة الأولى هي نقطة مرجعية للون الأبيض.
يؤدي ذلك إلى قفل السطوع العادي على النقطة البيضاء في النطاق العادي الديناميكية (SDR) التي يتم ضبطها حسب الإضاءة المحيطة بالشاشة، ما يمنع الارتفاعات المفاجئة في السطوع.
الركيزة الثانية هي المرجع الأساسي لـ SDR الخاص بصنّاع المحتوى.
يؤدي ذلك إلى الحفاظ على منحنى ربط درجات الألوان الدقيق الذي حدّده صانع المحتوى عند تحويل الفيديو إلى SDR.
ثالثًا، لدينا منحنيات كسب تكيُّفية لمساحة العرض، والتي تعمل على زيادة سطوع الأجزاء الساطعة بشكل حتمي لتتطابق مع مساحة العرض في الوقت الفعلي لتقنية النطاق العالي الديناميكية (HDR) على الشاشة، ليتطابق المحتوى مع البيئة التي يتم عرضه فيها.
وتتوفّر هذه الميزة في ExoPlayer تلقائيًا في Media3 1.11 على الأجهزة التي تعمل بنظام التشغيل Android 17 والإصدارات الأحدث.
ويستند Eclipsa Video إلى معايير مفتوحة للتوافق مع مختلف المجالات.
لذلك، هذا هو الوقت الأنسب لإتاحة تقنية HDR.
في ما يتعلّق بالصوت، لدينا Eclipsa Audio، وهو تنسيق صوت مكاني ثلاثي الأبعاد مفتوح المصدر وغير خاضع لحقوق الملكية.
ويستند إلى مواصفات IAMF الصادرة عن Alliance for Open Media.
ويجمع هذا التنسيق بين الصوت المحيطي القائم على المشهد والصوت القائم على القناة في بث واحد مع عروض تقديمية مختلطة متعددة.
يتيح ذلك إجراء تعديلات على العناصر، مثل تحسين الحوار، وسيعمل على كل شيء بدءًا من سماعات الرأس الاستيريو وحتى إعدادات الصوت المحيطي.
هناك مساران للتشغيل في Media3.
في الإصدار 17 من Android والإصدارات الأحدث، سيعرض كل من ExoPlayer وAndroid Audio Framework محتوى Eclipsa Audio تلقائيًا.
أما في الإصدار 16 من Android والإصدارات الأقدم، فيظل لديك خيار آخر.
يمكنك تفعيل إضافة IAMF من خلال مكتبة إضافات Media3 اختيارية.
وعلى صعيد إنشاء المحتوى، يمكنك كتابة مقاطع صوتية بتنسيق IAMF باستخدام أدوات دمج Media3.
ولكنّ توفير تنسيقات عالية الجودة ليس سوى نصف القصة.
وعلينا تقديم هذه الجودة بكفاءة.
أودّ الآن التحدّث عن كيفية مساهمة Media3 في جعل مسارات المعالجة أسرع وأكثر كفاءة.
مثل العديد من الميزات التي تناولناها اليوم، تتوفّر معظم تحسينات الأداء هذه تلقائيًا من خلال تحديث إصدار Media3 باستمرار.
ومن الأمثلة على الميزات التي نقدّمها ميزة "تخفيف حمل الصوت" بتنسيق PCM، والتي يمكنها توجيه تشغيل الصوت إلى وحدة معالجة منخفضة الطاقة.
في تحليلنا الأولي، لاحظنا توفيرًا في الطاقة يصل إلى %20 عند تشغيل الصوت والشاشة مطفأة.
بالإضافة إلى ميزة "تفريغ الصوت بتعديل الرمز النبضي"، لدينا ميزة "الجدولة الديناميكية" التي تتيح مزامنة عمليات تنشيط حلقة العرض الداخلية في ExoPlayer مع معدّل VSYNC ومدى توفّر مخزن وحدة فك الترميز المؤقت.
لدينا أيضًا ميزة "الإحماء المسبق للعرض"، والتي تتيح لك إجراء إحماء مسبق لعرض فيديو ثانوي، ما يسمح بالانتقال الفوري بين قوائم التشغيل وتسهيل دمج الإعلانات.
لدينا أيضًا ميزة رصد المشغّل المتوقّف والتخزين المؤقت المُسبَق، والتي ترصد تلقائيًا برامج التخزين المؤقت وبرامج العرض المتوقّفة وتحدّد ذاكرة التخزين المؤقت المُسبَق بشكل واضح.
هناك وضع آخر مثالي لدينا وهو وضع التقديم والترجيع.
وهذا مخصّص للتطبيقات التي يتصفّح فيها المستخدمون المحتوى بشكل متكرر.
عند تفعيل وضع التقديم والترجيع، يصبح سحب شريط التقديم والترجيع أكثر سلاسة واستجابة، كما هو موضّح هنا.
بالمناسبة، هذا مع إيقاف وضع التقديم والترجيع.
وعلى اليسار، يظهر وضع التقديم والترجيع مفعَّلاً.
شكرًا لك.
شكرًا لك.
حسنًا.
لنتحدّث الآن عن "سلامة نظام Android".
علينا احترام مواردنا المشتركة من ذاكرة الوصول العشوائي ووحدة المعالجة المركزية والبطارية، ومع تشديد Android لقواعده حول تعزيز الصوت والخدمات التي تعمل في المقدّمة، يمكن أن تواجه التطبيقات التي تدير خدماتها الأولية والصور النقطية بعض الأخطاء في كثير من الأحيان.
ومن بين هذه الاستثناءات، استثناءات عدم السماح ببدء الخدمة التي تعمل في المقدّمة أو استثناءات نفاد الذاكرة في Binder.
لذلك، عزّزنا أمان جلسة خدمة الوسائط ومدير إشعارات الوسائط، بالإضافة إلى مسارات الصور النقطية بعدة طرق، بما في ذلك منع طلبات خدمات تعمل في المقدّمة القديمة وتقييد ذاكرة الأعمال الفنية وما شابه ذلك.
ولكن الخلاصة بالنسبة إليك هي أنّه إذا كنت تستخدم Media3 وتتّبع الإرشادات العادية، سيتم التعامل مع جميع قيود صحة النظام هذه نيابةً عنك بدون أي تأثير.
أخيرًا، أودّ التحدّث عن ميزة ستتوفّر قريبًا في Media3.
عندما يحين وقت ترميز الفيديو أو تصديره، قد يكون تنوّع الأجهزة أحد أكبر التحديات.
تستخدم العديد من التطبيقات إعدادات ترميز موحّدة، ما يؤدي إلى جودة وأداء غير مثاليين.
لهذا السبب، أنشأنا CodecDB.
تستند CodecDB إلى قياسات شاملة للفيديوهات في الواقع باستخدام مقاييس جودة موحّدة ومعايير أداء الأجهزة على جميع أجهزة Android.
لنطّلِع على ذلك عمليًا.
على اليمين، لدينا إطار من فيديو التقطناه على هاتف استخدمنا فيه إعدادات فيديو عامة جدًا قد نستخدمها على العديد من الأجهزة.
وعلى يسار الشاشة، استخدمنا الإعدادات المثلى على مستوى الجهاز التي حصلنا عليها من CodecDB.
إذًا، أين تكمن الاختلافات؟
إذا نظرت إلى المناطق ذات التفاصيل العالية، فسترى أكبر فرق في الجودة.
لذا، انظر إلى وضوح فروع الأشجار والشجيرات أو حدة حافة المبنى.
ما نحصل عليه هنا هو تجربة مشاهدة أفضل بكثير تستفيد من إمكانات جهاز معيّن من خلال الاستفادة من CodecDB.
واستخدام CodecDB في الرمز بسيط للغاية.
val config = CodecDb.onDevice(context)
.forEncodingFallingBackTo {
constraints -> createFallbackConfig(constraints)
}
.findOptimalSettings()
.forEditing()
.withResolution(1920, 1080, /* frameRate= */ 30.0)
.withAnyCodec()
.targetingQuality(VideoEncoder.QUALITY_LEVEL_VERY_GOOD)
.get()ستستخدم نمطًا سلسًا لإنشاء المراحل يوجّهك خلال كل مَعلمة مطلوبة.
لذا، عليك تحديد حالة الاستخدام.
نستخدم هنا للتعديل.
يُعلم ذلك CodecDB بأنّك تستخدم مسار التصدير أو تحويل الترميز في Media3.
ومن حالات الاستخدام الأخرى التي يمكنك ضبطها، على سبيل المثال، التقاط الصور بالكاميرا إذا كنت تستخدمها معها.
ستتمكّن أيضًا من ضبط الدقة ومعدّل عرض الإطارات المستهدَفَين.
بعد ذلك، بدلاً من تخمين ما إذا كان الفيديو بدقة 1080p يحتاج إلى 8 ميغابت في الثانية أو 15 ميغابت في الثانية، يمكنك إخبار CodecDB ببرنامج الترميز الذي تريد استخدامه وطلب هدف جودة إدراكية.
في هذه الحالة، نستخدم مستوى الجودة "جيد جدًا".
في الخلفية، تبحث CodecDB عن منحنيات قياس الأداء التجريبية لشريحة SOC المحددة وتعرض مَعلمات برامج الترميز والتشفير المثالية.
نتوقّع إطلاق CodecDB في Media3 قريبًا جدًا، وعندما يتم ذلك، سيستخدم أنواع Media3 الأصلية.
وبالتالي، سيكون من السهل دمجها مع رمز Media3 الحالي.
حسنًا
لقد استعرضنا اليوم الكثير من الوظائف الرائعة في Media3، لذا دعونا نلخّصها.
تعرّفت اليوم على وحدات واجهة المستخدم الجديدة في Compose، وCastPlayer الجديد، ومسار التعديل المُجدَّد، والعديد من التأثيرات الجديدة التي يمكنك استخدامها في Media3.
لقد رأينا أيضًا أدوات جديدة لاسترداد البيانات الوصفية والإطارات.
وتعرّفنا على الدعم الجديد لتكنولوجيا البث وتحسين الأداء والأدوات المساعدة.
Media3 هي مكتبة مفتوحة المصدر، لذا يمكنك الاطّلاع على كيفية تنفيذنا لكل هذه الميزات في مستودع GitHub.
اعتبارًا من اليوم، أصبح أحدث إصدار ثابت من Media3 هو 1.11.1.
يمكنك أيضًا الاطّلاع على الإصدار 1.12.0، وهو حاليًا في مرحلة الإصدار الأولي.
ويمكنك اتّباع هذا الرابط المختصر الذي سينقلك إلى مستندات المطوّرين حول Media3.
شكرًا جزيلاً لكم، وأتمنى لكم الاستمتاع ببقية فعاليات مؤتمر Android Developer Summit.
@Composable
fun videoScreen() {
Player(viewModel.exoPlayer)
}Player(viewModel.exoPlayer)Player(
player = viewModel.exoPlayer,
bottomControls = {
PlayerDefaults.BottomControls(
player,
left = {}, right = {},
above = {
PositionAndDurationText(
player, separator = " • ")
},
)
},
)Player(
player = viewModel.exoPlayer,
centerControls = {},
bottomControls = { ...
above = {
Row(Modifier.fillMaxWidth()) {
PositionAndDurationText(...)
Spacer(Modifier.weight(1f))
PlaybackSpeedToggleButton(player)
MuteButton(player)
PlayPauseButton(player)
}
},
)
}Player(
...
bottomControls = { ...
above = {
...
MuteButton(player,
painter = { painterResource(
if (showMuted)
R.drawable.muted
else
R.drawable.unmuted )})
}
},
)MediaSession.Builder(context, player).build()class PlaybackService : MediaSessionService() {
private lateinit var mediaSession:
MediaSession
override fun onGetSession(
info: ControllerInfo) : MediaSession?
= mediaSession
}val player = ExoPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()Cast.getSingletonInstance(context).initialize()
val player = CastPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()Cast.getSingletonInstance(context).initialize()
val exoPlayer = ExoPlayer.Builder(context).build()
val player = CastPlayer.Builder(context)
.setLocalPlayer(exoPlayer).build()
mediaSession = MediaSession.Builder(context,player)
.build()Player(
exoPlayer,
centerControls = {}
bottomControls = { /* customized */ },
)Player(
castPlayer,
centerControls = {}
bottomControls = { /* customized */ },
topControls = {
PlayerDefaults.TopControls(castPlayer) {
MediaRouteButton(
Modifier.align(Alignment.TopEnd))
}
},
)public interface FrameProcessor
extends AutoCloseable {
interface Listener {
void onWakeup();
void onError(
VideoFrameProcessingException exception);
void onFrameProcessed(
Frame frame,
@Nullable SyncFenceWrapper onCompleteFence);
}
interface Factory {
FrameProcessor create(
FrameWriter output,
Executor listenerExecutor,
Listener listener);
}
boolean queue(List<AsyncFrame> frames);
void signalEndOfStream();
}val frameProcessorFactory = EnhancementSessionDecorator.Builder(/* context = */ this, baseGlFactory)
.setTonemappingEnabled(true)
.setDeblurAndDenoiseEnabled(true)
.setUpscaleVideoEnabled(true)
.build()
val transformerBuilder = Transformer.Builder(/* context = */ this)
.setFrameProcessorFactory(frameProcessorFactory)val editedMediaItem =
EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
.setEffects(
Effects(
listOf(
soundSeparationAudioProcessor
),
emptyList(), // video effects
)
)
.build()val soundSeparationAudioProcessor =
SoundSeparationAudioProcessor(
getApplication(),
mainExecutor,
object : SoundSeparationAudioProcessor.Listener {
override fun onResult(channelToAudioType: SparseIntArray) {
for (i in 0 until channelToAudioType.size()) {
val channelIndex = channelToAudioType.keyAt(i)
val audioType = channelToAudioType.valueAt(i)
val label = toAudioTypeName(audioType)
detectedChannelVolumes.add(ChannelVolume(channelIndex, audioType, label))
}
}
},
)MetadataRetriever.Builder(context, mediaItem).build().use { retriever ->
// 1. Duration (in microseconds)
val durationUs = retriever.retrieveDurationUs().await()
// 2. Track formats & container metadata (reuses the same preparation pass)
val trackGroups = retriever.retrieveTrackGroups().await()
for (i in 0 until trackGroups.length) {
val format = trackGroups[i].getFormat(0)
// Video/Audio properties: format.width, format.height, format.colorInfo …
// Rich metadata: SlowMotionData, MotionPhotoMetadata, Mp4TimestampData …
val metadata = format.metadata
}
// 3. Stream structure & manifest info
val timeline = retriever.retrieveTimeline().await()
}FrameExtractor.Builder(context, mediaItem)
.setSeekParameters(SeekParameters.CLOSEST_SYNC) // Fast keyframe seeking
.setEffects(listOf(Presentation.createForHeight(360))) // GPU downscaling
.setExtractHdrFrames(true) // Optional: HLG HDR or UltraHDR
.build()
.use { extractor ->
// Representative thumbnail
val thumbnail = extractor.getThumbnail().await()
// Frame at specific timestamp (bitmap + actual presentationTimeMs)
val frame = extractor.getFrame(positionMs = 5_000L).await()
}val config = CodecDb.onDevice(context)
.forEncodingFallingBackTo {
constraints -> createFallbackConfig(constraints)
}
.findOptimalSettings()
.forEditing()
.withResolution(1920, 1080, /* frameRate= */ 30.0)
.withAnyCodec()
.targetingQuality(VideoEncoder.QUALITY_LEVEL_VERY_GOOD)
.get()