העברת Camera1 ל- CameraX

אם האפליקציה שלכם משתמשת בכיתה המקורית Camera ('Camera1'), שהוצאה משימוש החל מ-Android 5.0‏ (רמת API 21), מומלץ מאוד לעדכן אותה ל-Android camera API מודרני. ב-Android יש את CameraX (ממשק API סטנדרטי וחזק למצלמה של Jetpack) ואת Camera2 (ממשק API ברמה נמוכה של מסגרת). ברוב המקרים, מומלץ להעביר את האפליקציה ל-CameraX. הנה תיאור הסיבות לכך:

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

במדריך הזה מפורטים תרחישים נפוצים לאפליקציות מצלמה. כל תרחיש כולל הטמעה של Camera1 והטמעה של CameraX לצורך השוואה.

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

CameraController

CameraProvider

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

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

לפני ההעברה

השוואה בין השימוש ב-CameraX לבין השימוש ב-Camera1

יכול להיות שהקוד ייראה שונה, אבל העקרונות הבסיסיים ב-Camera1 וב-CameraX דומים מאוד. CameraX מספקת פונקציונליות מופשטת של המצלמה לתרחישים לדוגמה, וכתוצאה מכך משימות רבות שהמפתחים היו צריכים לבצע ב-Camera1 מטופלות באופן אוטומטי על ידי CameraX. ב-CameraX יש ארבע UseCase שאפשר להשתמש בהן למגוון משימות במצלמה: Preview,‏ ImageCapture,‏ VideoCapture ו-ImageAnalysis.

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

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

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

לפני שנכנס לפרטים, נספק סקירה כללית על UseCase של CameraX ועל הקשר שלהם לאפליקציית Camera1. (הקונספטים של CameraX מופיעים בכחול והקונספטים של Camera1 מופיעים בירוק).

CameraX

הגדרת CameraController / CameraProvider
תצוגה מקדימה ImageCapture VideoCapture ImageAnalysis
ניהול המשטח של התצוגה המקדימה והגדרתו במצלמה הגדרת PictureCallback וקריאה ל-takePicture() ב-Camera ניהול ההגדרות של Camera ו-MediaRecorder בסדר ספציפי קוד ניתוח מותאם אישית שנבנה על גבי Surface של התצוגה המקדימה
קוד ספציפי למכשיר
ניהול רוטציה והתאמה לעומס (scaling) של מכשירים
ניהול סשנים במצלמה (בחירת מצלמה, ניהול מחזור חיים)

Camera1

תאימות וביצועים ב-CameraX

CameraX תומכת במכשירים עם Android 5.0 (רמת API‏ 21) ואילך. מדובר ביותר מ-98% ממכשירי Android הקיימים. CameraX נועד לטפל באופן אוטומטי בהבדלים בין מכשירים, וכך לצמצם את הצורך בקוד ספציפי למכשיר באפליקציה. בנוסף, אנחנו בודקים יותר מ-150 מכשירים פיזיים בכל הגרסאות של Android החל מגרסה 5.0 במעבדת הבדיקה של CameraX. אפשר לעיין ברשימה המלאה של המכשירים שקיימים כרגע ב-Test Lab.

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

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

מושגים בפיתוח ל-Android

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

  • קישור תצוגות יוצר סיווג קישור לקובצי הפריסה של ה-XML, ומאפשר להפנות לתצוגות ב-Activities בקלות, כפי שמתואר בכמה קטעי קוד בהמשך. יש כמה הבדלים בין קישור תצוגה לבין findViewById() (הדרך הקודמת להפנות לתצוגות), אבל בקוד שבהמשך תוכלו להחליף את השורות של קישור התצוגה בקריאה דומה ל-findViewById().
  • קורוטינים אסינכרונים הם תבנית תכנון של תכנות בו-זמנית שנוספה ל-Kotlin 1.3, וניתן להשתמש בה כדי לטפל בשיטות של CameraX שמחזירות ListenableFuture. קל יותר לעשות זאת באמצעות ספריית Concurrent של Jetpack, החל מגרסה 1.1.0. כדי להוסיף לאפליקציה פונקציית קורוטין אסינכרוני:
    1. מוסיפים את implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") לקובץ Gradle.
    2. צריך להוסיף קוד של CameraX שמחזיר ListenableFuture לבלוק של launch או לפונקציה מושעה.
    3. מוסיפים קריאה ל-await() לקריאה לפונקציה שמחזירה ListenableFuture.
    4. להבנה עמוקה יותר של אופן הפעולה של פונקציות רפליקות, קראו את המדריך הפעלת פונקציית רפליקות.

העברה של תרחישים נפוצים

בקטע הזה מוסבר איך להעביר תרחישים נפוצים מ-Camera1 ל-CameraX. כל תרחיש מכיל הטמעה של Camera1, הטמעה של CameraX CameraProvider והטמעה של CameraX CameraController.

בחירת מצלמה

באפליקציית המצלמה, אחד הדברים הראשונים שתרצו להציע הוא אפשרות לבחור מצלמות שונות.

Camera1

ב-Camera1, אפשר להפעיל את הפונקציה Camera.open() ללא פרמטרים כדי לפתוח את המצלמה האחורית הראשונה, או להעביר מזהה שלם של המצלמה שרוצים לפתוח. דוגמה למה שיכול לקרות:

// Camera1: select a camera from id.

// Note: opening the camera is a non-trivial task, and it shouldn't be
// called from the main thread, unlike CameraX calls, which can be
// on the main thread since CameraX kicks off background threads
// internally as needed.

private fun safeCameraOpen(id: Int): Boolean {
    return try {
        releaseCameraAndPreview()
        camera = Camera.open(id)
        true
    } catch (e: Exception) {
        Log.e(TAG, "failed to open camera", e)
        false
    }
}

private fun releaseCameraAndPreview() {
    preview?.setCamera(null)
    camera?.release()
    camera = null
}

CameraX: ‏ CameraController

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

זהו קוד CameraX לשימוש במצלמה האחורית שמוגדרת כברירת מחדל עם CameraController:

// CameraX: select a camera with CameraController

var cameraController = LifecycleCameraController(baseContext)
val selector = CameraSelector.Builder()
    .requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
cameraController.cameraSelector = selector

CameraX: ‏ CameraProvider

דוגמה לבחירת המצלמה הקדמית כברירת מחדל באמצעות CameraProvider (אפשר להשתמש במצלמה הקדמית או האחורית באמצעות CameraController או CameraProvider):

// CameraX: select a camera with CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Set up UseCases (more on UseCases in later scenarios)
    var useCases:Array = ...

    // Set the cameraSelector to use the default front-facing (selfie)
    // camera.
    val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

    } catch(exc: Exception) {
        Log.e(TAG, "UseCase binding failed", exc)
    }
})

...

// Call startCamera in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

אם אתם רוצים לבחור איזו מצלמה תיבחר, תוכלו לעשות זאת גם ב-CameraX באמצעות CameraProvider, באמצעות קריאה ל-getAvailableCameraInfos(). הפונקציה הזו מחזירה אובייקט CameraInfo שבעזרתו אפשר לבדוק מאפיינים מסוימים של המצלמה, כמו isFocusMeteringSupported(). לאחר מכן תוכלו להמיר אותו ל-CameraSelector כדי להשתמש בו כמו בדוגמאות שלמעלה באמצעות השיטה CameraInfo.getCameraSelector().

אפשר לקבל פרטים נוספים על כל מצלמה באמצעות השימוש בכיתה Camera2CameraInfo. קוראים ל-getCameraCharacteristic() עם מפתח לנתוני המצלמה הרצויים. בכיתה CameraCharacteristics מפורטת רשימה של כל המפתחות שאפשר לשלוח עליהם שאילתות.

דוגמה לשימוש בפונקציה מותאמת אישית של checkFocalLength() שאפשר להגדיר בעצמכם:

// CameraX: get a cameraSelector for first camera that matches the criteria
// defined in checkFocalLength().

val cameraInfo = cameraProvider.getAvailableCameraInfos()
    .first { cameraInfo ->
        val focalLengths = Camera2CameraInfo.from(cameraInfo)
            .getCameraCharacteristic(
                CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
            )
        return checkFocalLength(focalLengths)
    }
val cameraSelector = cameraInfo.getCameraSelector()

הצגת תצוגה מקדימה

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

בנוסף, ב-Camera1 צריך להחליט אם להשתמש ב-TextureView או ב-SurfaceView כמשטח התצוגה המקדימה. לכל אחת מהאפשרויות יש יתרונות וחסרונות, ובכל מקרה, ב-Camera1 צריך לטפל בכיוון ובשינוי התצוגה בצורה נכונה. לעומת זאת, ל-PreviewView ב-CameraX יש הטמעות בסיסיות גם ל-TextureView וגם ל-SurfaceView. CameraX מחליט איזו הטמעה הכי מתאימה בהתאם לגורמים כמו סוג המכשיר וגרסת Android שבה פועלת האפליקציה. אם כל אחת מההטמעות תואמת, תוכלו להצהיר על ההעדפה שלכם באמצעות PreviewView.ImplementationMode. באפשרות COMPATIBLE נעשה שימוש ב-TextureView לתצוגה המקדימה, ובערך PERFORMANCE נעשה שימוש ב-SurfaceView (כשהדבר אפשרי).

Camera1

כדי להציג תצוגה מקדימה, צריך לכתוב מחלקה משלכם של Preview עם הטמעה של הממשק android.view.SurfaceHolder.Callback, שמשמשים להעברת נתוני תמונות מחומרת המצלמה לאפליקציה. לאחר מכן, כדי להתחיל את התצוגה המקדימה של התמונה בשידור חי, צריך להעביר את הכיתה Preview לאובייקט Camera.

// Camera1: set up a camera preview.

class Preview(
        context: Context,
        private val camera: Camera
) : SurfaceView(context), SurfaceHolder.Callback {

    private val holder: SurfaceHolder = holder.apply {
        addCallback(this@Preview)
        setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        // The Surface has been created, now tell the camera
        // where to draw the preview.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: IOException) {
                Log.d(TAG, "error setting camera preview", e)
            }
        }
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // Take care of releasing the Camera preview in your activity.
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int,
                                w: Int, h: Int) {
        // If your preview can change or rotate, take care of those
        // events here. Make sure to stop the preview before resizing
        // or reformatting it.
        if (holder.surface == null) {
            return  // The preview surface does not exist.
        }

        // Stop preview before making changes.
        try {
            camera.stopPreview()
        } catch (e: Exception) {
            // Tried to stop a non-existent preview; nothing to do.
        }

        // Set preview size and make any resize, rotate or
        // reformatting changes here.

        // Start preview with new settings.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: Exception) {
                Log.d(TAG, "error starting camera preview", e)
            }
        }
    }
}

class CameraActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding
    private var camera: Camera? = null
    private var preview: Preview? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Create an instance of Camera.
        camera = getCameraInstance()

        preview = camera?.let {
            // Create the Preview view.
            Preview(this, it)
        }

        // Set the Preview view as the content of the activity.
        val cameraPreview: FrameLayout = viewBinding.cameraPreview
        cameraPreview.addView(preview)
    }
}

CameraX: ‏ CameraController

ב-CameraX, המפתחים צריכים לנהל הרבה פחות דברים. אם משתמשים ב-CameraController, צריך להשתמש גם ב-PreviewView. המשמעות היא ש-Preview UseCase הוא משוער, כך שההגדרה קלה יותר:

// CameraX: set up a camera preview with a CameraController.

class MainActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Create the CameraController and set it on the previewView.
        var cameraController = LifecycleCameraController(baseContext)
        cameraController.bindToLifecycle(this)
        val previewView: PreviewView = viewBinding.cameraPreview
        previewView.controller = cameraController
    }
}

CameraX: ‏ CameraProvider

כשמשתמשים ב-CameraProvider של CameraX, לא צריך להשתמש ב-PreviewView, אבל עדיין קל יותר להגדיר את התצוגה המקדימה מאשר ב-Camera1. לצורך הדגמה, בדוגמה הזו נעשה שימוש ב-PreviewView, אבל אם יש לכם צרכים מורכבים יותר, תוכלו לכתוב SurfaceProvider מותאם אישית כדי להעביר ל-setSurfaceProvider().

כאן, הערך Preview UseCase לא מובן מאליו כמו ב-CameraController, ולכן צריך להגדיר אותו:

// CameraX: set up a camera preview with a CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Create Preview UseCase.
    val preview = Preview.Builder()
        .build()
        .also {
            it.setSurfaceProvider(
                viewBinding.viewFinder.surfaceProvider
            )
        }

    // Select default back camera.
    val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

    } catch(exc: Exception) {
        Log.e(TAG, "UseCase binding failed", exc)
    }
})

...

// Call startCamera() in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

הקשה כדי להתמקד

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

Camera1

כדי להטמיע את התכונה 'הקשה כדי להתמקד' ב-Camera1, צריך לחשב את המיקוד האופטימלי Area כדי לציין איפה Camera צריך לנסות להתמקד. הערך של Area מועבר אל setFocusAreas(). בנוסף, צריך להגדיר מצב התמקדות תואם ב-Camera. אזור ההתמקדות משפיע רק אם מצב ההתמקדות הנוכחי הוא FOCUS_MODE_AUTO,‏ FOCUS_MODE_MACRO,‏ FOCUS_MODE_CONTINUOUS_VIDEO או FOCUS_MODE_CONTINUOUS_PICTURE.

כל Area הוא מלבן עם משקל שצוין. המשקל הוא ערך בין 1 ל-1,000, והוא משמש לקביעת סדר העדיפויות של מוקד Areas אם מוגדרים כמה מוקדים. בדוגמה הזו נעשה שימוש רק ב-Area אחד, ולכן ערך המשקל לא משנה. הקואורדינטות של המלבן נעות בין -1000 ל-1000. הנקודה הימנית העליונה היא (-1000, -1000). הנקודה השמאלית התחתונה היא (1000, 1000). הכיוון הוא ביחס לכיוון החיישן, כלומר, מה שהחיישן רואה. הכיוון לא מושפע מהסיבוב או מההיפוך של Camera.setDisplayOrientation(), לכן צריך להמיר את הקואורדינטות של אירוע המגע לקואורדינטות של החיישן.

// Camera1: implement tap-to-focus.

class TapToFocusHandler : Camera.AutoFocusCallback {
    private fun handleFocus(event: MotionEvent) {
        val camera = camera ?: return
        val parameters = try {
            camera.getParameters()
        } catch (e: RuntimeException) {
            return
        }

        // Cancel previous auto-focus function, if one was in progress.
        camera.cancelAutoFocus()

        // Create focus Area.
        val rect = calculateFocusAreaCoordinates(event.x, event.y)
        val weight = 1  // This value's not important since there's only 1 Area.
        val focusArea = Camera.Area(rect, weight)

        // Set the focus parameters.
        parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO)
        parameters.setFocusAreas(listOf(focusArea))

        // Set the parameters back on the camera and initiate auto-focus.
        camera.setParameters(parameters)
        camera.autoFocus(this)
    }

    private fun calculateFocusAreaCoordinates(x: Int, y: Int) {
        // Define the size of the Area to be returned. This value
        // should be optimized for your app.
        val focusAreaSize = 100

        // You must define functions to rotate and scale the x and y values to
        // be values between 0 and 1, where (0, 0) is the upper left-hand side
        // of the preview, and (1, 1) is the lower right-hand side.
        val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000
        val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000

        // Calculate the values for left, top, right, and bottom of the Rect to
        // be returned. If the Rect would extend beyond the allowed values of
        // (-1000, -1000, 1000, 1000), then crop the values to fit inside of
        // that boundary.
        val left = max(normalizedX - (focusAreaSize / 2), -1000)
        val top = max(normalizedY - (focusAreaSize / 2), -1000)
        val right = min(left + focusAreaSize, 1000)
        val bottom = min(top + focusAreaSize, 1000)

        return Rect(left, top, left + focusAreaSize, top + focusAreaSize)
    }

    override fun onAutoFocus(focused: Boolean, camera: Camera) {
        if (!focused) {
            Log.d(TAG, "tap-to-focus failed")
        }
    }
}

CameraX: ‏ CameraController

CameraController מקשיב לאירועי המגע של PreviewView כדי לטפל באופן אוטומטי בהקשה להתמקד. אפשר להפעיל ולהשבית את התכונה 'הקשה להתמקד' באמצעות setTapToFocusEnabled(), ולבדוק את הערך באמצעות פונקציית ה-getter המתאימה isTapToFocusEnabled().

השיטה getTapToFocusState() מחזירה אובייקט LiveData למעקב אחרי שינויים במצב המיקוד ב-CameraController.

// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView,
// with handlers you can define for focused, not focused, and failed states.

val tapToFocusStateObserver = Observer { state ->
    when (state) {
        CameraController.TAP_TO_FOCUS_NOT_STARTED ->
            Log.d(TAG, "tap-to-focus init")
        CameraController.TAP_TO_FOCUS_STARTED ->
            Log.d(TAG, "tap-to-focus started")
        CameraController.TAP_TO_FOCUS_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focus successful)")
        CameraController.TAP_TO_FOCUS_NOT_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focused unsuccessful)")
        CameraController.TAP_TO_FOCUS_FAILED ->
            Log.d(TAG, "tap-to-focus failed")
    }
}

cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)

CameraX: ‏ CameraProvider

כשמשתמשים ב-CameraProvider, צריך לבצע כמה הגדרות כדי שהתכונה 'הקשה להתמקד' תפעל. בדוגמה הזו ההנחה היא שאתם משתמשים ב-PreviewView. אם לא, צריך להתאים את הלוגיקה כך שתחול על Surface בהתאמה אישית.

כך משתמשים ב-PreviewView:

  1. מגדירים גלאי תנועות כדי לטפל באירועי הקשה.
  2. באמצעות אירוע הקשה, יוצרים MeteringPoint באמצעות MeteringPointFactory.createPoint().
  3. בעזרת MeteringPoint, יוצרים FocusMeteringAction.
  4. עם האובייקט CameraControl ב-Camera (שהוחזר מ-bindToLifecycle()), קוראים ל-startFocusAndMetering() ומעבירים את FocusMeteringAction.
  5. (אופציונלי) משיבים להודעה FocusMeteringResult.
  6. מגדירים את גלאי התנועות כך שישיב לאירועי מגע בקטע PreviewView.setOnTouchListener().
// CameraX: implement tap-to-focus with CameraProvider.

// Define a gesture detector to respond to tap events and call
// startFocusAndMetering on CameraControl. If you want to use a
// coroutine with await() to check the result of focusing, see the
// "Android development concepts" section above.
val gestureDetector = GestureDetectorCompat(context,
    object : SimpleOnGestureListener() {
        override fun onSingleTapUp(e: MotionEvent): Boolean {
            val previewView = previewView ?: return
            val camera = camera ?: return
            val meteringPointFactory = previewView.meteringPointFactory
            val focusPoint = meteringPointFactory.createPoint(e.x, e.y)
            val meteringAction = FocusMeteringAction
                .Builder(meteringPoint).build()
            lifecycleScope.launch {
                val focusResult = camera.cameraControl
                    .startFocusAndMetering(meteringAction).await()
                if (!result.isFocusSuccessful()) {
                    Log.d(TAG, "tap-to-focus failed")
                }
            }
        }
    }
)

...

// Set the gestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    // See pinch-to-zooom scenario for scaleGestureDetector definition.
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

תנועת צביטה כדי לשנות את מרחק התצוגה

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

Camera1

יש שתי דרכים לבצע זום באמצעות Camera1. השיטה Camera.startSmoothZoom() מפעילה אנימציה מרמת הזום הנוכחית לרמת הזום שאתם מעבירים. השיטה Camera.Parameters.setZoom() תעביר אתכם ישירות לרמת הזום שתעבירו. לפני שמשתמשים באחת מהן, צריך להקיש על isSmoothZoomSupported() או על isZoomSupported(), בהתאמה, כדי לוודא ששיטות הזום הרלוונטיות שדרושות זמינות במצלמה.

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

// Camera1: implement pinch-to-zoom.

// Define a scale gesture detector to respond to pinch events and call
// setZoom on Camera.Parameters.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : ScaleGestureDetector.OnScaleGestureListener {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return false
            val parameters = try {
                camera.parameters
            } catch (e: RuntimeException) {
                return false
            }

            // In case there is any focus happening, stop it.
            camera.cancelAutoFocus()

            // Set the zoom level on the Camera.Parameters, and set
            // the Parameters back onto the Camera.
            val currentZoom = parameters.zoom
            parameters.setZoom(detector.scaleFactor * currentZoom)
        camera.setParameters(parameters)
            return true
        }
    }
)

// Define a View.OnTouchListener to attach to your preview view.
class ZoomTouchListener : View.OnTouchListener {
    override fun onTouch(v: View, event: MotionEvent): Boolean =
        scaleGestureDetector.onTouchEvent(event)
}

// Set a ZoomTouchListener to handle touch events on your preview view
// if zoom is supported by the current camera.
if (camera.getParameters().isZoomSupported()) {
    view.setOnTouchListener(ZoomTouchListener())
}

CameraX: ‏ CameraController

בדומה להקשה כדי להתמקד, CameraController מקשיב לאירועי המגע של PreviewView כדי לטפל באופן אוטומטי בהצמדה כדי להגדיל או להקטין. אפשר להפעיל ולהשבית את התכונה 'הצמדה כדי להגדיל' באמצעות setPinchToZoomEnabled(), ולבדוק את הערך באמצעות פונקציית ה-getter המתאימה isPinchToZoomEnabled().

השיטה getZoomState() מחזירה אובייקט LiveData למעקב אחרי שינויים ב-ZoomState ב-CameraController.

// CameraX: track the state of pinch-to-zoom over the Lifecycle of
// a PreviewView, logging the linear zoom ratio.

val pinchToZoomStateObserver = Observer { state ->
    val zoomRatio = state.getZoomRatio()
    Log.d(TAG, "ptz-zoom-ratio $zoomRatio")
}

cameraController.getZoomState().observe(this, pinchToZoomStateObserver)

CameraX: ‏ CameraProvider

כדי להשתמש בתנועת הצמדה כדי להתקרב או להתרחק ב-CameraProvider, צריך לבצע כמה הגדרות. אם אתם לא משתמשים ב-PreviewView, תצטרכו להתאים את הלוגיקה כך שתתאים ל-Surface בהתאמה אישית.

כך משתמשים ב-PreviewView:

  1. הגדרת גלאי תנועות להגדלה כדי לטפל באירועי צביטה.
  2. מקבלים את ZoomState מהאובייקט Camera.CameraInfo, שבו המופע Camera מוחזר כשקוראים ל-bindToLifecycle().
  3. אם לשדה ZoomState יש ערך zoomRatio, שומרים אותו כיחס הזום הנוכחי. אם אין zoomRatio ב-ZoomState, המערכת תשתמש בשיעור הזום שמוגדר כברירת מחדל ב-camera (1.0).
  4. כדי לקבוע את יחס הזום החדש, מכפילים את יחס הזום הנוכחי ב-scaleFactor ומעבירים את התוצאה ל-CameraControl.setZoomRatio().
  5. מגדירים את גלאי התנועות כך שיגיב לאירועי מגע בקטע PreviewView.setOnTouchListener().
// CameraX: implement pinch-to-zoom with CameraProvider.

// Define a scale gesture detector to respond to pinch events and call
// setZoomRatio on CameraControl.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : SimpleOnGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return
            val zoomState = camera.cameraInfo.zoomState
            val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f
            camera.cameraControl.setZoomRatio(
                detector.scaleFactor * currentZoomRatio
            )
        }
    }
)

...

// Set the scaleGestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        // See pinch-to-zooom scenario for gestureDetector definition.
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

צילום תמונה

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

Camera1

קודם צריך להגדיר ב-Camera1 את Camera.PictureCallback כדי לנהל את נתוני התמונה כשהם נדרשים. דוגמה פשוטה ל-PictureCallback לטיפול בנתוני תמונות JPEG:

// Camera1: define a Camera.PictureCallback to handle JPEG data.

private val picture = Camera.PictureCallback { data, _ ->
    val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run {
        Log.d(TAG,
              "error creating media file, check storage permissions")
        return@PictureCallback
    }

    try {
        val fos = FileOutputStream(pictureFile)
        fos.write(data)
        fos.close()
    } catch (e: FileNotFoundException) {
        Log.d(TAG, "file not found", e)
    } catch (e: IOException) {
        Log.d(TAG, "error accessing file", e)
    }
}

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

// Camera1: call takePicture on Camera instance, passing our PictureCallback.

camera?.takePicture(null, null, picture)

CameraX: ‏ CameraController

CameraController של CameraX שומר על הפשטות של Camera1 לצילום תמונות באמצעות הטמעה של שיטת takePicture() משלו. כאן מגדירים פונקציה להגדרת רשומה ב-MediaStore ולצילום תמונה שתשמור שם.

// CameraX: define a function that uses CameraController to take a photo.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun takePhoto() {
   // Create time stamped name and MediaStore entry.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }

   // Create output options object which contains file + metadata.
   val outputOptions = ImageCapture.OutputFileOptions
       .Builder(context.getContentResolver(),
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
       .build()

   // Set up image capture listener, which is triggered after photo has
   // been taken.
   cameraController.takePicture(
       outputOptions,
       ContextCompat.getMainExecutor(this),
       object : ImageCapture.OnImageSavedCallback {
           override fun onError(e: ImageCaptureException) {
               Log.e(TAG, "photo capture failed", e)
           }

           override fun onImageSaved(
               output: ImageCapture.OutputFileResults
           ) {
               val msg = "Photo capture succeeded: ${output.savedUri}"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       }
   )
}

CameraX: ‏ CameraProvider

צילום תמונה באמצעות CameraProvider פועל כמעט באותו אופן כמו באמצעות CameraController, אבל קודם צריך ליצור ולקשור ImageCapture UseCase כדי שיהיה אובייקט שאפשר לקרוא עליו ל-takePicture():

// CameraX: create and bind an ImageCapture UseCase.

// Make a reference to the ImageCapture UseCase at a scope that can be accessed
// throughout the camera logic in your app.
private var imageCapture: ImageCapture? = null

...

// Create an ImageCapture instance (can be added with other
// UseCase definitions).
imageCapture = ImageCapture.Builder().build()

...

// Bind UseCases to camera (adding imageCapture along with preview here, but
// preview is not required to use imageCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, imageCapture)

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

// CameraX: define a function that uses CameraController to take a photo.

private fun takePhoto() {
    // Get a stable reference of the modifiable ImageCapture UseCase.
    val imageCapture = imageCapture ?: return

    ...

    // Call takePicture on imageCapture instance.
    imageCapture.takePicture(
        ...
    )
}

צילום סרטון

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

כפי שתראו, גם כאן CameraX מטפל בחלק גדול מהמורכבות.

Camera1

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

  1. פותחים את המצלמה.
  2. מכינים את התצוגה המקדימה ומתחילים אותה (אם באפליקציה מוצג הסרטון שמצולם, בדרך כלל זה המצב).
  3. כדי לבטל את נעילת המצלמה לשימוש על ידי MediaRecorder, צריך להתקשר למספר Camera.unlock().
  4. מגדירים את ההקלטה באמצעות קריאה לשיטות הבאות ב-MediaRecorder:
    1. מחברים את המכונה של Camera ל-setCamera(camera).
    2. התקשרות אל setAudioSource(MediaRecorder.AudioSource.CAMCORDER).
    3. התקשרות אל setVideoSource(MediaRecorder.VideoSource.CAMERA).
    4. כדי להגדיר את האיכות, מתקשרים למספר setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)). אפשרויות האיכות מפורטות במאמר CamcorderProfile.
    5. התקשרות אל setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()).
    6. אם באפליקציה יש תצוגה מקדימה של הסרטון, צריך להתקשר למספר setPreviewDisplay(preview?.holder?.surface).
    7. התקשרות אל setOutputFormat(MediaRecorder.OutputFormat.MPEG_4).
    8. התקשרות אל setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT).
    9. התקשרות אל setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT).
    10. צריך להתקשר למספר prepare() כדי להשלים את ההגדרה של MediaRecorder.
  5. כדי להתחיל את ההקלטה, מתקשרים למספר MediaRecorder.start().
  6. כדי להפסיק את ההקלטה, צריך לבצע קריאה ל-methods האלה. שוב, יש לפעול לפי הסדר המדויק הזה:
    1. התקשרות אל MediaRecorder.stop().
    2. אפשר גם להסיר את ההגדרה הנוכחית של MediaRecorder באמצעות קריאה ל-MediaRecorder.reset().
    3. התקשרות אל MediaRecorder.release().
    4. כדי לנעול את המצלמה כך שאפשר יהיה להשתמש בה בסשנים עתידיים של MediaRecorder, צריך להפעיל את הפונקציה Camera.lock().
  7. כדי להפסיק את התצוגה המקדימה, צריך להתקשר למספר Camera.stopPreview().
  8. לבסוף, כדי לשחרר את Camera כדי שתהליכים אחרים יוכלו להשתמש בו, צריך להפעיל את Camera.release().

אלה כל השלבים בשילוב:

// Camera1: set up a MediaRecorder and a function to start and stop video
// recording.

// Make a reference to the MediaRecorder at a scope that can be accessed
// throughout the camera logic in your app.
private var mediaRecorder: MediaRecorder? = null
private var isRecording = false

...

private fun prepareMediaRecorder(): Boolean {
    mediaRecorder = MediaRecorder()

    // Unlock and set camera to MediaRecorder.
    camera?.unlock()

    mediaRecorder?.run {
        setCamera(camera)

        // Set the audio and video sources.
        setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
        setVideoSource(MediaRecorder.VideoSource.CAMERA)

        // Set a CamcorderProfile (requires API Level 8 or higher).
        setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH))

        // Set the output file.
        setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())

        // Set the preview output.
        setPreviewDisplay(preview?.holder?.surface)

        setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
        setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)

        // Prepare configured MediaRecorder.
        return try {
            prepare()
            true
        } catch (e: IllegalStateException) {
            Log.d(TAG, "preparing MediaRecorder failed", e)
            releaseMediaRecorder()
            false
        } catch (e: IOException) {
            Log.d(TAG, "setting MediaRecorder file failed", e)
            releaseMediaRecorder()
            false
        }
    }
    return false
}

private fun releaseMediaRecorder() {
    mediaRecorder?.reset()
    mediaRecorder?.release()
    mediaRecorder = null
    camera?.lock()
}

private fun startStopVideo() {
    if (isRecording) {
        // Stop recording and release camera.
        mediaRecorder?.stop()
        releaseMediaRecorder()
        camera?.lock()
        isRecording = false

        // This is a good place to inform user that video recording has stopped.
    } else {
        // Initialize video camera.
        if (prepareVideoRecorder()) {
            // Camera is available and unlocked, MediaRecorder is prepared, now
            // you can start recording.
            mediaRecorder?.start()
            isRecording = true

            // This is a good place to inform the user that recording has
            // started.
        } else {
            // Prepare didn't work, release the camera.
            releaseMediaRecorder()

            // Inform user here.
        }
    }
}

CameraX: ‏ CameraController

באמצעות CameraController של CameraX, אפשר להפעיל או להשבית את ImageCapture,‏ VideoCapture ו-ImageAnalysis UseCase בנפרד, כל עוד אפשר להשתמש ברשימה של תרחישי השימוש בו-זמנית. הפקודות ImageCapture ו-ImageAnalysis UseCase מופעלות כברירת מחדל, ולכן לא נדרשת קריאה לפקודה setEnabledUseCases() כדי לצלם תמונה.

כדי להשתמש ב-CameraController להקלטת וידאו, קודם צריך להשתמש ב-setEnabledUseCases() כדי לאפשר את VideoCapture UseCase.

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

כדי להתחיל לצלם סרטון, אפשר להפעיל את הפונקציה CameraController.startRecording(). הפונקציה הזו יכולה לשמור את הסרטון שצילמתם ב-File, כפי שמוצג בדוגמה שבהמשך. בנוסף, צריך להעביר Executor וכיתה שמטמיעה את OnVideoSavedCallback כדי לטפל בקריאות חזרה (callbacks) של הצלחה ושגיאה. כשרוצים לסיים את ההקלטה, מקישים על CameraController.stopRecording().

הערה: אם אתם משתמשים ב-CameraX בגרסה 1.3.0-alpha02 ואילך, יש פרמטר נוסף AudioConfig שמאפשר להפעיל או להשבית את הקלטת האודיו בסרטון. כדי להפעיל הקלטת אודיו, צריך לוודא שיש לכם הרשאות גישה למיקרופון. בנוסף, השיטה stopRecording() הוסרה בגרסה 1.3.0-alpha02, ו-startRecording() מחזירה אובייקט Recording שאפשר להשתמש בו כדי להשהות, להמשיך ולהפסיק את צילום הסרטון.

// CameraX: implement video capture with CameraController.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

// Define a VideoSaveCallback class for handling success and error states.
class VideoSaveCallback : OnVideoSavedCallback {
    override fun onVideoSaved(outputFileResults: OutputFileResults) {
        val msg = "Video capture succeeded: ${outputFileResults.savedUri}"
        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
        Log.d(TAG, msg)
    }

    override fun onError(videoCaptureError: Int, message: String,
                         cause: Throwable?) {
        Log.d(TAG, "error saving video: $message", cause)
    }
}

private fun startStopVideo() {
    if (cameraController.isRecording()) {
        // Stop the current recording session.
        cameraController.stopRecording()
        return
    }

    // Define the File options for saving the video.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
        .format(System.currentTimeMillis())

    val outputFileOptions = OutputFileOptions
        .Builder(File(this.filesDir, name))
        .build()

    // Call startRecording on the CameraController.
    cameraController.startRecording(
        outputFileOptions,
        ContextCompat.getMainExecutor(this),
        VideoSaveCallback()
    )
}

CameraX: ‏ CameraProvider

אם אתם משתמשים ב-CameraProvider, צריך ליצור VideoCapture UseCase ולהעביר אובייקט Recorder. ב-Recorder.Builder אפשר להגדיר את איכות הווידאו, ואם רוצים גם FallbackStrategy, שיטפל במקרים שבהם המכשיר לא יכול לעמוד במפרטי האיכות הרצויים. לאחר מכן תוכלו לקשר את המכונה VideoCapture ל-CameraProvider באמצעות שאר הערכים של UseCase.

// CameraX: create and bind a VideoCapture UseCase with CameraProvider.

// Make a reference to the VideoCapture UseCase and Recording at a
// scope that can be accessed throughout the camera logic in your app.
private lateinit var videoCapture: VideoCapture
private var recording: Recording? = null

...

// Create a Recorder instance to set on a VideoCapture instance (can be
// added with other UseCase definitions).
val recorder = Recorder.Builder()
    .setQualitySelector(QualitySelector.from(Quality.FHD))
    .build()
videoCapture = VideoCapture.withOutput(recorder)

...

// Bind UseCases to camera (adding videoCapture along with preview here, but
// preview is not required to use videoCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, videoCapture)

בשלב הזה אפשר לגשת ל-Recorder בנכס videoCapture.output. אפשר להשתמש ב-Recorder כדי להתחיל לצלם סרטונים ששמורים ב-File, ב-ParcelFileDescriptor או ב-MediaStore. בדוגמה הזו נעשה שימוש ב-MediaStore.

ב-Recorder יש כמה שיטות לקריאה כדי להכין אותו. קוראים לפונקציה prepareRecording() כדי להגדיר את אפשרויות הפלט של MediaStore. אם לאפליקציה יש הרשאה להשתמש במיקרופון של המכשיר, צריך להפעיל גם את withAudioEnabled(). לאחר מכן, קוראים ל-start() כדי להתחיל את ההקלטה, ומעבירים הקשר ומעקב אירועים מסוג Consumer<VideoRecordEvent> לטיפול באירועי הקלטת וידאו. אם הפעולה בוצעה בהצלחה, אפשר להשתמש ב-Recording המוחזר כדי להשהות, להמשיך או לעצור את ההקלטה.

// CameraX: implement video capture with CameraProvider.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun startStopVideo() {
   val videoCapture = this.videoCapture ?: return

   if (recording != null) {
       // Stop the current recording session.
       recording.stop()
       recording = null
       return
   }

   // Create and start a new recording session.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
       .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
       }
   }

   val mediaStoreOutputOptions = MediaStoreOutputOptions
       .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
       .setContentValues(contentValues)
       .build()

   recording = videoCapture.output
       .prepareRecording(this, mediaStoreOutputOptions)
       .withAudioEnabled()
       .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
           when(recordEvent) {
               is VideoRecordEvent.Start -> {
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.stop_capture)
                       isEnabled = true
                   }
               }
               is VideoRecordEvent.Finalize -> {
                   if (!recordEvent.hasError()) {
                       val msg = "Video capture succeeded: " +
                           "${recordEvent.outputResults.outputUri}"
                       Toast.makeText(
                           baseContext, msg, Toast.LENGTH_SHORT
                       ).show()
                       Log.d(TAG, msg)
                   } else {
                       recording?.close()
                       recording = null
                       Log.e(TAG, "video capture ends with error",
                             recordEvent.error)
                   }
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.start_capture)
                       isEnabled = true
                   }
               }
           }
       }
}

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

יש לנו כמה אפליקציות CameraX מלאות במאגר GitHub של דוגמאות למצלמה. הדוגמאות האלה מראות איך התרחישים במדריך הזה משתלבים באפליקציה מלאה ל-Android.

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