הקרנת מדיה

ממשקי ה-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<Intent> 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.

שחזור משאבים

האפליקציה צריכה לרשום את פונקציית ה-callback‏ 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) QPR1 מופיע צ'יפ חדש בסרגל הסטטוס, גדול ובולט, שמיועד להתריע למשתמשים על הקרנת מסך מתמשכת. המשתמשים יכולים להקיש על הצ'יפ כדי להפסיק את השיתוף, ההעברה או הצילום של המסך שלהם.

ב-Android 15 QPR1 ואילך, הקרנת המסך מופסקת באופן אוטומטי כשמסך המכשיר נעול.

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

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

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