הקרנת מדיה

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

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

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

שלושה ייצוגים לרשת המדיה

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

תצוגה של מכשיר אמיתי שמוקרנת על תצוגה וירטואלית. Contents of
              virtual display written to application-provided `Surface`.
איור 1. מסך של מכשיר אמיתי או חלון של אפליקציה שמוקרנים על מסך וירטואלי. תצוגה וירטואלית שנכתבה ל-Surface שסופק על ידי האפליקציה.

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

תצוגה אמיתית

מתחילים הפעלה של הקרנת מדיה על ידי קבלת טוקן שמעניק לאפליקציה את היכולת לצלם את התוכן של מסך המכשיר או של חלון האפליקציה. הטוקן מיוצג על ידי מופע של המחלקה 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). (פרטים נוספים זמינים בקטע גודל ההקרנה של מדיה).

פני השטח

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

החל מ-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, צריך להשתמש בשיטה WindowMetricsCalculator computeMaximumWindowMetrics() מהספרייה WindowManager של Jetpack.

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

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

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

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

התאמה אישית

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

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

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

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

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

שחזור משאבים

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

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

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

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

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

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

  • יוצרים מופע של MediaProjection.Callback.

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

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

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

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