הקרנת מדיה

ממשקי ה-API של android.media.projection, שהוצגו ב-Android 5 (רמת API‏ 21), מאפשרים לצלם את התוכן של מסך המכשיר כסטרימינג של מדיה, שאפשר להפעיל, להקליט או להעביר למכשירים אחרים, כמו טלוויזיות.

ב-Android 14 (רמת API 34) נוספה תכונת שיתוף מסך של אפליקציה, שמאפשרת למשתמשים לשתף חלון של אפליקציה אחת במקום את כל מסך המכשיר, ללא קשר למצב החלון. כשמשתפים מסך של אפליקציה, שורת הסטטוס, סרגל הניווט, ההתראות ורכיבים אחרים של ממשק המשתמש של המערכת לא מוצגים במסך המשותף – גם אם משתמשים בשיתוף המסך של האפליקציה כדי לצלם את האפליקציה במסך מלא. רק התוכן של האפליקציה שנבחרה ישותף.

שיתוף מסך האפליקציה מבטיח את פרטיות המשתמשים, מגביר את הפרודוקטיביות שלהם ומאפשר להם לבצע כמה משימות בו-זמנית. לשם כך, המשתמשים יכולים להפעיל כמה אפליקציות אבל להגביל את שיתוף התוכן לאפליקציה אחת בלבד.

שלושה ייצוגים של תצוגה

הקרנת מדיה מתעדת את התוכן של מסך המכשיר או חלון האפליקציה, ולאחר מכן מקרינה את התמונה שצולמה על מסך וירטואלי שמריץ את התמונה ב-Surface.

תצוגת המכשיר האמיתי מוקרנת על מסך וירטואלי. התוכן של המסך הווירטואלי נכתב ב-'Surface' שסופק על ידי האפליקציה.
איור 1. מסך המכשיר או חלון האפליקציה מוקרנים על המסך הווירטואלי. תצוגה וירטואלית שנכתבה ב-Surface שסופקה על ידי האפליקציה.

האפליקציה מספקת את Surface באמצעות MediaRecorder,‏ SurfaceTexture או ImageReader, שמשתמשים בתוכן של המסך שצולם ומאפשרים לנהל תמונות שעברן רינדור ב-Surface בזמן אמת. אפשר לשמור את התמונות כקלטת או להעביר אותן לטלוויזיה או למכשיר אחר.

תצוגה אמיתית

כדי להתחיל סשן של הקרנת מדיה, צריך לקבל אסימון שמעניק לאפליקציה את היכולת לתעד את התוכן של מסך המכשיר או חלון האפליקציה. האסימון מיוצג על ידי מופע של הכיתה MediaProjection.

משתמשים בשיטה getMediaProjection() של שירות המערכת MediaProjectionManager כדי ליצור מכונה של MediaProjection כשמתחילים פעילות חדשה. מתחילים את הפעילות עם כוונה (intent) מהשיטה createScreenCaptureIntent() כדי לציין פעולת צילום מסך:

Kotlin

val mediaProjectionManager = getSystemService(MediaProjectionManager::class.java)
var mediaProjection : MediaProjection
val startMediaProjection = registerForActivityResult( StartActivityForResult() ) { result -> if (result.resultCode == RESULT_OK) { mediaProjection = mediaProjectionManager .getMediaProjection(result.resultCode, result.data!!) } }
startMediaProjection.launch(mediaProjectionManager.createScreenCaptureIntent())

Java

final MediaProjectionManager mediaProjectionManager =
    getSystemService(MediaProjectionManager.class);
final MediaProjection[] mediaProjection = new MediaProjection[1];
ActivityResultLauncher startMediaProjection = registerForActivityResult( new StartActivityForResult(), result -> { if (result.getResultCode() == Activity.RESULT_OK) { mediaProjection[0] = mediaProjectionManager .getMediaProjection(result.getResultCode(), result.getData()); } } );
startMediaProjection.launch(mediaProjectionManager.createScreenCaptureIntent());

מסך וירטואלי

הרכיב המרכזי של הקרנת מדיה הוא המסך הווירטואלי, שיוצרים באמצעות קריאה ל-createVirtualDisplay() במכונה של MediaProjection:

Kotlin

virtualDisplay = mediaProjection.createVirtualDisplay(
                     "ScreenCapture",
                     width,
                     height,
                     screenDensity,
                     DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                     surface,
                     null, null)

Java

virtualDisplay = mediaProjection.createVirtualDisplay(
                     "ScreenCapture",
                     width,
                     height,
                     screenDensity,
                     DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                     surface,
                     null, null);

הפרמטרים width ו-height מציינים את המאפיינים של המסך הווירטואלי. כדי לקבל ערכים לרוחב ולגובה, משתמשים בממשקי ה-API של WindowMetrics שהוצגו ב-Android 11 (רמת API ‏30). (פרטים נוספים זמינים בקטע גודל הקרנת המדיה).

Surface

מגדירים את גודל משטח הקרנת המדיה כך שיפיק פלט ברזולוציה המתאימה. כדאי להגדיל את המשטח (רזולוציה נמוכה) כדי להעביר את המסך לטלוויזיות או למסכי מחשב, ולהקטין אותו (רזולוציה גבוהה) כדי לצלם את המסך של המכשיר.

החל מ-Android 12L‏ (API ברמה 32), כשהמערכת מבצעת עיבוד (רנדור) של תוכן שצולם על פני השטח, היא משנה את הגודל של התוכן באופן אחיד תוך שמירה על יחס הגובה-רוחב, כך ששני המימדים של התוכן (רוחב וגובה) יהיו שווים או קטנים מהמימדים התואמים של פני השטח. לאחר מכן, התוכן שצילמתם ימורכז על פני השטח.

הגישה של Android 12L לשינוי קנה המידה משפרת את ההעברה של המסך לטלוויזיות ולמסכים גדולים אחרים, על ידי הגדלת התמונה על המסך תוך שמירה על יחס הגובה-רוחב הנכון.

הרשאה לשימוש בשירות שפועל בחזית

אם האפליקציה שלכם מטרגטת ל-Android 14 ואילך, מניפסט האפליקציה צריך לכלול הצהרת הרשאה לסוג השירות שפועל בחזית mediaProjection:

<manifest ...>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
    <application ...>
        <service
            android:name=".MyMediaProjectionService"
            android:foregroundServiceType="mediaProjection"
            android:exported="false">
        </service>
    </application>
</manifest>

מפעילים את שירות הקרנת המדיה באמצעות קריאה ל-startForeground().

אם לא מציינים את סוג השירות שפועל בחזית בקריאה, ברירת המחדל של הסוג היא מספר שלם בייטבי של סוגי השירותים שפועלים בחזית שהוגדרו במניפסט. אם לא צוינו סוגי שירותים במניפסט, המערכת תשליך את השגיאה MissingForegroundServiceTypeException.

האפליקציה חייבת לבקש מהמשתמשים הסכמה לפני כל סשן הקרנת מדיה. סשן הוא קריאה יחידה ל-createVirtualDisplay(). צריך להשתמש באסימון MediaProjection רק פעם אחת כדי לבצע את הקריאה.

ב-Android 14 ואילך, השיטה createVirtualDisplay() גורמת להשלכת SecurityException אם האפליקציה מבצעת אחת מהפעולות הבאות:

  • העברת מופע Intent שהוחזר מ-createScreenCaptureIntent() ל-getMediaProjection() יותר מפעם אחת
  • קריאות ל-createVirtualDisplay() יותר מפעם אחת באותו מופע MediaProjection

גודל הקרנת המדיה

הקרנת מדיה יכולה לתעד את כל מסך המכשיר או חלון של אפליקציה, ללא קשר למצב החלון.

גודל ראשוני

כשמשתמשים בהקרנת מדיה במסך מלא, האפליקציה צריכה לקבוע את גודל המסך של המכשיר. כשמשתפים מסך מאפליקציה, האפליקציה לא תוכל לקבוע את גודל המסך שצולם עד שהמשתמש יבחר את אזור הצילום. לכן, הגודל הראשוני של כל הקרנת מדיה הוא הגודל של מסך המכשיר.

משתמשים בשיטה WindowManager getMaximumWindowMetrics() של הפלטפורמה כדי להחזיר אובייקט WindowMetrics של מסך המכשיר, גם אם אפליקציית המארח של הקרנת המדיה נמצאת במצב חלונות מרובים ותופסת רק חלק מהמסך.

כדי לשמור על תאימות עד לרמת API ‏14, צריך להשתמש ב-method‏ WindowMetricsCalculator computeMaximumWindowMetrics() מהספרייה WindowManager של Jetpack.

קוראים ל-method WindowMetrics getBounds() כדי לקבל את הרוחב והגובה של המסך במכשיר.

שינויים בגודל

הגודל של הקרנת המדיה יכול להשתנות כשמסובבים את המכשיר או כשהמשתמש בוחר חלון של אפליקציה כאזור הצילום בזמן שיתוף המסך של האפליקציה. יכול להיות שפרויקציית המדיה תהיה בפורמט letterbox אם התוכן שצולם הוא בגודל שונה ממדדי החלון המקסימלי שהתקבלו כשהגדרתם את פרויקציית המדיה.

כדי לוודא שהקרנת המדיה תואמת במדויק לגודל התוכן שצולם בכל אזור שצולם ובכל סיבוב של המכשיר, צריך להשתמש ב-callback‏ onCapturedContentResize() כדי לשנות את הגודל של הצילום. (למידע נוסף, עיינו בקטע התאמה אישית שבהמשך).

התאמה אישית

האפליקציה יכולה להתאים אישית את חוויית המשתמש בהקרנת המדיה באמצעות ממשקי ה-API הבאים של MediaProjection.Callback:

  • onCapturedContentVisibilityChanged(): מאפשרת לאפליקציית המארח (האפליקציה שהפעילה את הקרנת המדיה) להציג או להסתיר את התוכן המשותף.

    אפשר להשתמש בקריאה החוזרת הזו כדי להתאים אישית את ממשק המשתמש של האפליקציה על סמך העובדה שהאזור שצולם גלוי למשתמש. לדוגמה, אם האפליקציה שלכם גלויה למשתמש והיא מציגה את התוכן שצולם בממשק המשתמש של האפליקציה, והאפליקציה שצולמה גלויה גם למשתמש (כפי שמצוין באמצעות קריאה חוזרת (callback) הזו), המשתמש רואה את אותו תוכן פעמיים. אפשר להשתמש בקריאה החוזרת כדי לעדכן את ממשק המשתמש של האפליקציה ולהסתיר את התוכן שצולם, וכך לפנות מקום בפריסת האפליקציה לתוכן אחר.

  • onCapturedContentResize(): מאפשר לאפליקציית המארח לשנות את הגודל של הקרנת המדיה במסך הווירטואלי ושל הקרנת המדיה Surface על סמך גודל אזור התצוגה שצולם.

    האירוע מופעל בכל פעם שהתוכן שצולם – חלון של אפליקציה אחת או מסך המכשיר במלואו – משתנה בגודל (בגלל סיבוב המכשיר או מעבר של האפליקציה שצולמה למצב חלון אחר). אפשר להשתמש ב-API הזה כדי לשנות את הגודל של המסך הווירטואלי ושל המשטח, כדי לוודא שיחס הגובה-רוחב תואם לתוכן שמצולם ושלא מוצגת תמונת letterbox.

שחזור משאבים

האפליקציה צריכה לרשום את פונקציית ה-call back MediaProjection onStop() כדי לקבל הודעה כשסשן הקרנת המדיה מושהה ולא חוקי יותר. כשהסשן מושהה, האפליקציה צריכה לשחרר את המשאבים שהיא מחזיקה, כמו המסך הווירטואלי ופלטפורמת ההקרנה. סשן הקרנת מדיה מושהה לא יכול ליצור יותר מסך וירטואלי חדש, גם אם האפליקציה לא יצרה בעבר מסך וירטואלי להקרנת המדיה הזו.

המערכת מפעילה את קריאת החזרה (callback) כשהקרנת המדיה מסתיימת. הסיבות לכך יכולות להיות, בין היתר:

אם האפליקציה לא תירשם את הקריאה החוזרת, כל קריאה ל-createVirtualDisplay() תגרום להשלכת IllegalStateException.

ביטול ההסכמה

ב-Android מגרסה 14 ואילך, שיתוף המסך של האפליקציה מופעל כברירת מחדל. בכל סשן הקרנת מדיה, המשתמשים יכולים לשתף חלון של אפליקציה או את המסך כולו.

כדי לבטל את ההסכמה לשיתוף המסך של האפליקציה, אפשר להפעיל את השיטה createScreenCaptureIntent(MediaProjectionConfig) עם הארגומנט MediaProjectionConfig שמוחזר מהקריאה ל-createConfigForDefaultDisplay().

קריאה ל-createScreenCaptureIntent(MediaProjectionConfig) עם ארגומנט MediaProjectionConfig שמוחזר מקריאה ל-createConfigForUserChoice() זהה להתנהגות ברירת המחדל, כלומר קריאה ל-createScreenCaptureIntent().

אפליקציות שניתן לשנות את הגודל שלהן

תמיד צריך לוודא שאפליקציות הקרנת המדיה ניתנות לשינוי גודל (resizeableActivity="true"). אפליקציות שניתן לשנות את הגודל שלהן תומכות בשינויים בהגדרות המכשיר ובמצב 'חלונות מרובים' (ראו תמיכה במצב 'חלונות מרובים').

אם לא ניתן לשנות את הגודל של האפליקציה, היא צריכה לשלוח שאילתה לגבי גבולות התצוגה מההקשר של החלון ולהשתמש ב-getMaximumWindowMetrics() כדי לאחזר את WindowMetrics של אזור התצוגה המקסימלי שזמין לאפליקציה :

Kotlin

val windowContext = context.createWindowContext(context.display!!,
      WindowManager.LayoutParams.TYPE_APPLICATION, null)
val projectionMetrics = windowContext.getSystemService(WindowManager::class.java)
      .maximumWindowMetrics

Java

Context windowContext = context.createWindowContext(context.getDisplay(),
      WindowManager.LayoutParams.TYPE_APPLICATION, null);
WindowMetrics projectionMetrics = windowContext.getSystemService(WindowManager.class)
      .getMaximumWindowMetrics();

צ'יפ בשורת הסטטוס ועצירה אוטומטית

נקודות חולשה של הקרנת מסך חושפות נתונים פרטיים של משתמשים, כמו מידע פיננסי, כי המשתמשים לא מבינים שהמסך של המכשיר שלהם משותף.

ב-Android מגרסה 15 ואילך (רמת API 35 ואילך) מוצג צ'יפ גדול ובולט בסרגל הסטטוס כדי להתריע למשתמשים על הקרנת מסך מתמשכת. המשתמשים יכולים להקיש על הצ'יפ כדי להפסיק את השיתוף, ההעברה או ההקלטה של המסך. בנוסף, הקרנת המסך תיפסק באופן אוטומטי כשמסך המכשיר יינעל.

איור 2. צ'יפ בסרגל הסטטוס לשיתוף מסך, להעברה (cast) ולהקלטה.

כדי לבדוק את הזמינות של צ'יפ סרגל הסטטוס של הקרנת המדיה, מתחילים לשתף את המסך, לבצע העברה או להקליט. הצ'יפ אמור להופיע בשורת הסטטוס.

כדי לוודא שהאפליקציה משחררת משאבים ומעדכנת את ממשק המשתמש שלה כשהקרנת המסך מופסקת כתוצאה מאינטראקציה של המשתמש עם צ'יפ שורת המצב או הפעלה של מסך הנעילה, צריך לבצע את הפעולות הבאות:

  • יוצרים מכונה של MediaProjection.Callback.

  • מטמיעים את שיטת הקריאה החוזרת onStop(). ה-method נקרא כשהקרנת המסך נעצרת. משחררים את המשאבים שהאפליקציה שומרת ומעדכנים את ממשק המשתמש של האפליקציה לפי הצורך.

כדי לבדוק את הקריאה החוזרת, מקישים על הצ'יפ בשורת הסטטוס או נועלים את מסך המכשיר כדי להפסיק את הקרנת המסך. מוודאים שהשיטה onStop() נקראת והאפליקציה מגיבה כמצופה.

מקורות מידע נוספים

מידע נוסף על הקרנת מדיה זמין במאמר צילום של הפעלת וידאו ואודיו.