צריכת מדיה מהווה חלק גדול מהזמן שהמשתמשים מבלים במכשירים ניידים, ולכן חוויית ההפעלה צריכה להיות חלקה, יעילה מאוד וחסכונית בחיי הסוללה. בנוסף, המשתמשים מצפים שהחוויות האלה יעברו בצורה חלקה בין המכשירים שלהם, מטלפונים ל-Living Room ומעבר לכך.
איך עדכונים בארכיטקטורה של Media3 ו-ExoPlayer מאפשרים הפעלה חלקה במכשירים שונים. הדיון מתמקד ביצירת ממשקים רספונסיביים באמצעות ערכת הכלים Media3 Compose UI, ביטול הטעינה מראש בפידים באמצעות PreloadManager החדש והרחבת האפליקציה למסכי Living Room ול-Android XR באמצעות CastPlayer ואודיו מרחבי.
תמליל
אוקיי.
שלום לכולם.
קוראים לי טוני.
אני מהנדס בצוות המדיה של Android.
ויחד עם קריסטינה ודונובן, נדבר על איך Jetpack Media3 יכול לעזור לכם עם כל צורכי המדיה שלכם ב-Android.
Media3 היא ספריית התמיכה של Android לכל מה שקשור למדיה, בין אם מדובר בהפעלה באמצעות ExoPlayer, שיתוף מדיה באמצעות סשן מדיה ו-Cast, עריכת מדיה באמצעות Transformer או בדיקת מדיה, כמו חילוץ פריימים או מטא-נתונים.
Media3 מכסה את כל התחום הזה בקבוצת ספרייה אחת.
כספריית תמיכה, היא כלולה באפליקציה שלכם.
זה אומר שאנחנו יכולים להבטיח עקביות, כך שאותו קוד פועל בכל הגרסאות של מערכות ההפעלה ובכל המכשירים.
זה גם עוזר לשמור על האפשרות לקבל עדכונים.
כל תיקון באג שאנחנו מבצעים מגיע עד ל-API 24, והוא לא קשור למחזור הפצה של מערכת הפעלה כלשהי.
כספריית תמיכה, אנחנו יכולים לאפשר אפשרויות התאמה אישית עשירות מאוד, שלא היו אפשריות במסגרת של Android.
כספרייה ב-GitHub, אנחנו קודם כל קוד פתוח.
זה אומר שכל שינוי שאנחנו מבצעים זמין באופן מיידי ב-GitHub.
יש לנו Issue Tracker פעיל, ואנחנו מקבלים בקשות משיכה.
הדבר העיקרי שחשוב להדגיש כאן, בכל התרחישים לדוגמה האלה, הוא שאנחנו באמת דואגים לכל רמות הפירוט, החל מרכיבים ברמה גבוהה בממשק המשתמש ועד לממשקי API פשוטים ומוכנים לשימוש, אבל גם אפשרויות התאמה אישית מעמיקות, תמיכה רחבה בפורמטים ובסטנדרטים שונים, והביצועים והיעילות שעומדים בבסיס של כל זה.
Media3 מכסה את כל התחום הזה.
היום ננסה להתעמק בחלק מהנושאים האלה.
נדבר על מודול ממשק המשתמש החדש של Compose, על CastPlayer החדש שמאפשר לשתף בקלות רבה יותר עם סשן מדיה, על צינור עיבוד נתונים משופר לעריכה עם אפקטים מתקדמים חדשים, על כלים חדשים לאחזור מטא-נתונים ופריימים, על טכנולוגיית סטרימינג חדשה, במיוחד וידאו ואודיו של Eclipsa, ועל כמה כלי עזר לאיכות החוויה לטעינה מראש, לחיפוש לאורך ציר הזמן ולחיסכון בסוללה.
עכשיו נצלול ישר לנושא הראשון: איך Media3 יכול לעזור לכם ליצור רכיבים בממשק המשתמש למדיה.
לאחרונה פרסמנו שני מודולים חדשים: Media3 UI Compose ו-Media3 UI Compose Material 3.
כאן אפשר לראות קומפוננטה של נגן ברמה גבוהה, כפי שהיא הושקה ב-Media3 1.10 ללא שינויים נוספים.
@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.
אנחנו מספקים את אבני הבניין הפשוטות של ה-API, אבל גם את כל אפשרויות ההתאמה האישית שאתם צריכים כדי להתאים אותו עוד יותר.
אולי אתם תוהים איך המשתמש עובר בפועל בין הפעלה מקומית להפעלה מרחוק בהגדרה הזו.
הוא כבר פועל באופן מיידי עם תיבת הדו-שיח של מתג הפלט של המערכת בהתראת המדיה.
זה הכפתור הקטן בפינה.
אבל מה אם אתם רוצים להציג את הכפתור להפעלת Cast במסך ההפעלה כדי שיהיה קל יותר למצוא אותו?
זוכרים את ממשק המשתמש המותאם אישית של 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 API מורכבים מכמה רכיבים.
ראשית, Transformer הוא נקודת הכניסה הראשית לממשקי ה-API שלנו לעריכה.
Transformer מעבד, ממיר ומייצא מדיה לקובץ.
הבא בתור הוא CompositionPlayer, שמעבד תצוגות מקדימות בזמן אמת של קומפוזיציות המדיה שלכם.
כך תוכלו לראות תצוגה מקדימה של העריכות ולשפר אותן לפני הייצוא הסופי.
אחר כך יש לנו אפקטים.
הוא נותן לכם שליטה מלאה על עיבוד פריים אחר פריים כשאתם מוסיפים אפקטים חזותיים פשוטים או שכבות-על גרפיות.
ולבסוף, יש לנו את Muxer.
הוא משמש כשלב הפלט הסופי בצינור עיבוד הנתונים שלנו, מקבל טראקים מעובדים של אודיו, וידאו ומטא-נתונים, וכותב אותם בקובצי MP4 סטנדרטיים.
בשנה האחרונה חשבנו מחדש על תהליך עריכת הסרטונים ובנינו אותו מהיסוד. היום אנחנו שמחים לשתף את השיפורים המשמעותיים שביצענו.
למה בנינו מחדש את צינור עיבוד הנתונים?
לכן, הארכיטקטורה הקודמת הציבה כמה אתגרים.
קודם כל, הוא לא הצליח להפיץ מטא-נתונים בכל שלבי העיבוד.
הוא היה קשור באופן הדוק ל-OpenGL, מה שהגביל את הגמישות של כלים גרפיים מודרניים.
בנוסף, ה-API המורכב שלו הקשה על המפתחים להתאים אישית ולתחזק את האפליקציה.
כדי להתמודד עם האתגרים האלה, בנינו צינור עיבוד נתונים חדש לגמרי.
קודם כול, עכשיו הרבה יותר קל ליצור קומפוזיציות מורכבות עם כמה נכסים.
שנית, ההתנתקות מ-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 מקבל בעלות על מאגרי חומרה, האפליקציה יכולה לקרוא ולשנות נתונים ישירות במקום.
הגישה הזו, שנקראת zero-copy, מפחיתה את העומס על הזיכרון ומאפשרת עיבוד מהיר במיוחד.
אחת מהחברות שמנצלות את ממשקי ה-API החדשים לעריכה היא Adobe.
הצוות של Adobe Premiere השתמש בממשקי ה-API לעריכה של Media3 כבסיס לתשתית עריכת המדיה שלהם, כדי להבטיח תאימות טובה יותר בסביבה העסקית של Android, תוך שמירה על הגמישות שהם צריכים כדי להשתמש בצינור עיבוד נתונים מותאם אישית משלהם וב-WebGPU.
בנוסף, אני שמח להודיע שאנחנו משיקים אפקטים חדשים כחלק מגרסה 1.12 של Media3.
נתחיל עם שיפורי סרטונים.
מאחורי הקלעים, הארכיטקטורה שלנו מבוססת על הגדרה מודולרית.
האפליקציה שלכם משולבת עם Play Services של Media3 effect, שמתממשק עם המודול Play Services Enhancement.
המודול "שיפור סרטונים" כולל יכולות של מיפוי גוונים, הפחתת רעשים, הסרת טשטוש ושדרוג הרזולוציה.
כדי לשלב את Video Enhance בתהליך העבודה, צריך רק כמה שורות קוד.
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.
אפקט מעניין נוסף שמגיע ל-Media3 1.12 הוא Studio Sound.
התכונה "איכות אולפן" מנתחת באופן אוטומטי את טראק האודיו של הסרטון ומפרידה אותו לערוצים נפרדים כמו דיבור, רעשי הסביבה ומוזיקה.
יוצרים יכולים להפחית בקלות את רעשי הרקע של הרוח או הקהל, ולהדגיש את הדיבור והמוזיקה.
כדי לשלב את "איכות קול של אולפן", צריך להעביר את מעבד האודיו של "איכות קול של אולפן" בתוך אובייקט של אפקט.
val editedMediaItem =
EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
.setEffects(
Effects(
listOf(
soundSeparationAudioProcessor
),
emptyList(), // video effects
)
)
.build()הפעולה הזו מחברת את מעבד האודיו לקובץ המדיה.
כשיוצרים מעבד אודיו של "סאונד של אולפן", צריך לצרף את פונקציית ה-callback של המאזין כדי לקבל את מיפוי ערוצי האודיו המנותחים באמצעות 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, הוא משתלב בצורה חלקה עם פונקציות coroutine של Kotlin כדי ליצור קוד נקי שלא חוסם.
העיצוב שלו נקי יותר ופחות מועד לשגיאות, והוא משתמש באחזור אחד שאי אפשר לשנות לכל קובץ מדיה.
הוא פועל באופן עקבי בכל גרסאות ה-API ובכל המכשירים.
הוא תומך במגוון רחב של פורמטים של מדיה.
הנה דוגמה מעשית לשימוש ב-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()
}בעזרת coroutines של Kotlin, אתם יכולים לאחזר באופן אסינכרוני את משך הזמן, לבדוק את הפורמטים של הטראקים ואת המטא-נתונים, והכול בבלוק אחד נקי.
עכשיו נדבר על 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()
}אתם יוצרים מופע עם אפשרויות כמו פרמטרים של חיפוש, הקטנת GPU, אפקטים ותמיכה ב-HDR, ואז ממתינים באופן אסינכרוני לתמונות ממוזערות ולפריימים ספציפיים באמצעות קריאות השעיה נקיות.
ועכשיו, לדונבן.
תודה, Kristina.
עכשיו נעבור לדבר על חלק מהתמיכה הטכנית הבסיסית בסטרימינג שהוספנו ל-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 או אפילו יותר ניטים כדי שתוכלו לראות את התוכן.
טווח הבהירות של SDR רגיל הוא 100 עד 300.
וכמובן, HDR לא נועד להבהיר את המסך באופן גלובלי.
אתם רוצים להרחיב את הטווח הדינמי, כדי שיהיו לכם אזורים בהירים מקומיים לדברים כמו אש ואור שמש.
אבל הסמארטפון מוסיף כמה סיבוכים לעניין, כי אנחנו לוקחים אותו איתנו לכל מקום, לחדר חשוך בבית או לחוף ים שטוף שמש.
בנוסף, אפליקציות רבות משלבות רכיבים של ממשק משתמש ב-SDR עם תוכן ב-SDR ותוכן ב-HDR בפידים מעורבים, וזה יכול להוביל לאפקט המסנוור והצורם הזה שאתם יכולים לקבל לפעמים, במיוחד אם אתם גוללים בשעות הלילה המאוחרות.
עכשיו אנחנו מבינים למה כדאי להשתמש ב-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.
ב-Android מגרסה 17 ואילך, ExoPlayer ו-Android Audio Framework יעבדו באופן אוטומטי את Eclipsa Audio.
ב-Android מגרסה 16 ומטה, עדיין יש לכם אפשרות.
אפשר להפעיל את תוסף IAMF דרך ספריית תוספים אופציונלית של Media3.
בצד של היצירה, אתם יכולים לכתוב טראקים של אודיו בפורמט IAMF באמצעות מרבבי Media3.
אבל תמיכה בפורמטים באיכות גבוהה היא רק חלק מהעניין.
אנחנו צריכים לספק את האיכות הזו ביעילות.
עכשיו אני רוצה לדבר על האופן שבו Media3 הופך את צינורות עיבוד הנתונים שלנו למהירים ויעילים יותר.
כמו הרבה דברים שדיברנו עליהם היום, רוב השיפורים האלה בביצועים זמינים באופן מיידי, פשוט צריך לעדכן את גרסת Media3.
דוגמה אחת לתכונה כזו היא הפחתת עומס של אודיו ב-PCM, שיכולה לנתב את הפעלת האודיו ליחידת עיבוד עם צריכת חשמל נמוכה.
בניתוח הראשוני שלנו, ראינו חיסכון של עד 20% בצריכת החשמל בהפעלת אודיו כשהמסך כבוי.
בנוסף להעברת אודיו ב-PCM, יש לנו תזמון דינמי, שמתאים את ההתעוררויות של לולאת העיבוד הפנימית של ExoPlayer לזמינות של VSYNC ושל מאגר הנתונים הזמני של המפענח.
יש לנו גם חימום מראש של רכיב הרינדור, שמאפשר לחמם מראש רכיב רינדור משני של וידאו. כך אפשר לעבור באופן מיידי בין פלייליסטים ולחבר מודעות בקלות רבה יותר.
יש לנו גם זיהוי של נגן תקוע ושמירה מראש במטמון, שמזהים באופן אוטומטי מאגרים ומעבדי עיבוד תקועים ומגבילים את הזיכרון שנשמר מראש במטמון בצורה נקייה.
מצב אופטימלי נוסף שיש לנו הוא מצב חיפוש לאורך ציר הזמן.
התכונה הזו מתאימה לאפליקציות שבהן המשתמשים מחפשים לעיתים קרובות לאורך ציר הזמן של התוכן.
כמו שאפשר לראות כאן, כשמפעילים את מצב החיפוש לאורך ציר הזמן, הגרירה של סרגל החיפוש הרבה יותר חלקה ומהירה.
אגב, זה קורה כשמצב החיפוש לאורך ציר הזמן מושבת.
ובצד שמאל, אנחנו רואים שמצב החיפוש לאורך ציר הזמן מופעל.
תודה.
תודה.
טוב.
עכשיו נדבר על תקינות המערכת של Android.
אנחנו צריכים להתחשב במשאבים המשותפים של זיכרון ה-RAM, המעבד והסוללה. לכן, ככל ש-Android מחמירה את הכללים שלה לגבי הקשחת אודיו ושירותים שפועלים בחזית, אפליקציות שמנהלות שירותים גולמיים ומפות סיביות משלהן עלולות להיתקל בשגיאות לעיתים קרובות יותר.
יכול להיות שראיתם חריגות מסוג "לא ניתן להפעיל שירות שפועל בחזית" או חריגות מסוג "אין מספיק זיכרון ב-Binder".
לכן הקשחנו את הסשן של שירות המדיה ואת מנהל ההתראות של המדיה, וגם את צינורות עיבוד הנתונים של מפת הסיביות שלנו בכמה דרכים, כולל מניעת כוונות של שירות חזית לא עדכני והגבלת הזיכרון של הגרפיקה ודברים כאלה.
אבל השורה התחתונה היא שאם אתם משתמשים ב-Media3 ופועלים לפי ההנחיות הרגילות, כל ההגבלות האלה על תקינות המערכת מטופלות בשבילכם בלי השפעה.
ולסיום, אני רוצה לדבר על תכונה שתהיה זמינה בקרוב ב-Media3.
כשמגיע הזמן לקודד או לייצא סרטון, מגוון החומרות יכול להיות אחד האתגרים הגדולים ביותר.
הרבה אפליקציות משתמשות בהגדרת codec אחידה, מה שמוביל לאיכות ולביצועים לא אופטימליים.
לכן פיתחנו את 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 המדויק הזה ומחזיר את הפרמטרים האידיאליים של ה-codec והמקודד.
אנחנו צופים ש-CodecDB יושק ב-Media3 בקרוב מאוד, וכשזה יקרה, הוא ישתמש בסוגים מקוריים של Media3.
לכן, קל מאוד לשלב אותו עם קוד Media3 הקיים.
אוקיי.
ראינו היום הרבה פונקציונליות מדהימה ב-Media3, אז בואו נסכם.
היום למדתם על המודולים החדשים של ממשק המשתמש של Compose, על CastPlayer החדש, על צינור עיבוד הנתונים המשופר לעריכה ועל הרבה אפקטים חדשים שאפשר להשתמש בהם ב-Media3.
ראינו גם כלים חדשים לאחזור מטא-נתונים ופריימים.
וגם בדקנו את התמיכה בטכנולוגיית סטרימינג חדשה, את שיפור הביצועים ואת כלי השירות.
Media3 היא ספריית קוד פתוח, כך שאתם יכולים לראות איך הטמענו את כל התכונות האלה במאגר שלנו ב-GitHub.
נכון להיום, הגרסה היציבה האחרונה של Media3 היא 1.11.1.
אתם יכולים גם לבדוק את גרסה 1.12.0, שנמצאת כרגע בשלב אלפא.
בקישור הקצר הזה תמצאו את מאמרי העזרה למפתחים בנושא Media3.
תודה רבה, והמשך יום נעים.
@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()