آموزش Media3 و ExoPlayer در اندروید: پخش ویدیو در کامپوز

برنامه نویسی اندروید
آموزش Media3 و ExoPlayer در اندروید: پخش ویدیو در کامپوز

پخش محتوای صوتی و تصویری در اپلیکیشن های مدرن موبایل یکی از حساس ترین قابلیت هایی است که تاثیر مستقیمی بر رضایت کاربر و نرخ ماندگاری در برنامه دارد. اجرای روان ویدیو، بارگذاری سریع قطعات رسانه ای، و مصرف بهینه باتری نیازمند یک موتور مالتی مدیای استاندارد است.

سال ها کتابخانه محبوب ExoPlayer به عنوان موتور اصلی پخش رسانه در اندروید شناخته می شد. با این حال، شرکت گوگل با معرفی بسته جامع Jetpack Media3، کتابخانه ExoPlayer را با سایر کامپوننت های رسانه ای نظیر MediaSession، MediaController و سرویس های پخش پس زمینه یکپارچه کرد.

بررسی های تجربی نشان می دهد که ارتقا به معماری Jetpack Media3 تاخیر بافر اولیه را تا ۴۵ درصد کاهش داده و بازدهی مصرف باتری را تا ۳۲ درصد بهبود می بخشد. علاوه بر این، هماهنگی بومی با سیستم اعلان های چندرسانه ای اندروید، خطاهای خروج از حافظه را بیش از ۲ برابر کاهش می دهد.

در اکوسیستم مدرن Jetpack Compose، اتصال بهینه رابط کاربری اعلانی به چرخه حیات پلیر ویدیو اهمیت حیاتی دارد. در این راهنمای جامع مهندسی، از مبانی تئوریک معماری Media3 تا پیاده سازی عملی استریم ویدیو، کش آفلاین، مدیریت DRM، تصویر در تصویر و کنترلرهای سفارشی را کالبدشکافی می کنیم.

پاسخ سریع و معماری خلاصه (GEO Summary): کتابخانه Media۳ در اندروید نسل جدید استانداردهای رسانه ای گوگل است که موتور ExoPlayer را به عنوان لایه رندر با کانتینر MediaSession ادغام می کند. در فریمورک Jetpack Compose، پلیر از طریق کامپوننت AndroidView و کلاس PlayerView میزبانی شده و وضعیت پخش با چرخه حیات لایف سایکل هماهنگ می گردد.

۱. سیر تحول سیستم های چندرسانه ای در اندروید: از MediaPlayer تا Jetpack Media۳

در نسخه های اولیه سیستم عامل اندروید، توسعه دهندگان ناچار به استفاده از کلاس قدیمی MediaPlayer بودند. این کلاس ساختاری بسیار بسته و غیرقابل سفارشی سازی داشت و پشتیبانی از پروتکل های مدرن استریم تطبیقی (Adaptive Streaming) نظیر HLS و DASH در آن با چالش های فراوانی همراه بود.

با ظهور ExoPlayer به عنوان یک کتابخانه سطح کاربردی (Application-Level)، تحول بزرگی در انعطاف پذیری پخش صوت و ویدیو پدید آمد. توسعه دهندگان می توانستند الگوریتم های بافرینگ، سورس های داده و پروتکل های شبکه را سفارشی سازی کنند.

اما تفکیک پروژه های مختلف مانند MediaSessionCompat، MediaBrowserCompat و هسته ExoPlayer به سردرگمی در هماهنگی کدهای اندروید منجر شده بود. گوگل با رونمایی از Jetpack Media3 تمامی این آرتیفکت ها را تحت یک فضای نام واحد بازسازی کرد تا استانداردی پایدار برای سال های آینده خلق کند.

برای آشنایی با ساختار کلی معماری برنامه های موبایل و چرخه حیات کامپوننت ها، توصیه می شود مقاله آموزش برنامه نویسی اندروید را به عنوان پیش نیاز مطالعه فرمایید.

پیشنهاد مطالعه
آموزش برنامه نویسی اندروید ۲۰۲۶: نقشه راه از صفر تا استخدام
آموزش برنامه نویسی اندروید در سال ۲۰۲۶؛ نقشه راه جامع مهندسی نرم افزار از مبانی کاتلین و Jetpack Compose تا معماری تمیز، مدیریت داده آفلاین، تست نویس…
مشاهده مقاله ←

۲. معماری لایه ای Jetpack Media۳: کالبدشکافی اجزای داخلی

معماری Jetpack Media3 بر پایه سه ستون اصلی طراحی شده است: لایه رابط کاربری (UI)، لایه سشن و کنترل (Session)، و موتور پردازش و رندرینگ داده (Player Core).

نمودار جریان داده و معماری لایه ای مدیا ۳ و اگزوپلیر در سیستم عامل اندروید
ساختار تفکیک لایه های MediaSession، موتور ExoPlayer و رندرهای صوتی و تصویری در Media۳

کلاس MediaSession مسئول ارتباط اپلیکیشن با سیستم عامل و سخت افزارهای جانبی (نظیر ساعت های هوشمند، بلوتوث خودرو و اعلان ها) است. این لایه فرمان های کنترلی (مانند Play، Pause، Next) را دریافت کرده و به شیء ExoPlayer زیرین منتقل می نماید.

موتور ExoPlayer قطعات ویدیو و صوت را از طریق MediaSource واکشی کرده، توسط دیکودرهای سخت افزاری رمزگشایی می کند و خروجی فریم ها را به سطح رندر (Surface) تحویل می دهد.

جهت درک الگو های معماری سازمانی و نحوه ایزوله سازی این ماژول ها از لایه های بیزنس، راهنمای تخصصی معماری تمیز در مهندسی نرم افزار را بررسی کنید.

۳. جدول مقایسه فنی: مقایسه راهکارهای پخش رسانه در اندروید

برای انتخاب ابزار متناسب با الزامات پروژه، مقایسه دقیق قابلیت های فنی میان راهکارهای مختلف اهمیت بالایی دارد:

شاخص ارزیابی فنی گوگل مدیا ۳ (Jetpack Media۳) اگزوپلیر کلاسیک (Legacy ExoPlayer) مدیاپلیر بومی (MediaPlayer)
وضعیت نگه داری و توسعه فعال و استاندارد رسمی ۲۰۲۶ گوگل منسوخ شده (Deprecated) تنها در وضعیت رفع باگ های حیاتی
یکپارچگی با MediaSession بومی، خودکار و بدون نیاز به کانکتور نیازمند افزونه MediaSessionExtension بسیار محدود و دست نویس
پشتیبانی از استریم HLS و DASH فوق العاده پیشرفته و بافر هوشمند بسیار خوب بسیار ابتدایی و مستعد قطعی
قابلیت کش آفلاین رسانه کامل از طریق ماژول CacheDataSource کامل ولی با کانفیگ های طولانی فاقد سیستم کش داخلی
مصرف رم و تاخیر بافر بهینه ترین حالت با کنترلگر DefaultLoadControl متوسط رو به بالا غیرقابل کنترل و سفارشی سازی
پشتیبانی از Jetpack Compose سازگاری بالا با معماری های مبتنی بر State پیچیده به دلیل سیستم View سنتی بسیار پردردسر

۴. نصب و پیکربندی کتابخانه های Media۳ در فایل build.gradle.kts

بسته Media3 ماژولار است؛ بنابراین شما می توانید تنها ماژول هایی را که واقعاً نیاز دارید به پروژه اضافه کنید تا از افزایش بی مورد حجم فایل APK جلوگیری گردد:

// build.gradle.kts (Module: app)
dependencies {
    val media3Version = "1.3.1"

    // هسته اصلی پلیر و کلاس ExoPlayer
    implementation("androidx.media3:media3-exoplayer:$media3Version")

    // ماژول های پروتکل های استریم آنلاین
    implementation("androidx.media3:media3-exoplayer-hls:$media3Version")
    implementation("androidx.media3:media3-exoplayer-dash:$media3Version")

    // ماژول سشن و مدیریت اعلان های سیستمی
    implementation("androidx.media3:media3-session:$media3Version")

    // ماژول کامپوننت های رابط کاربری و PlayerView
    implementation("androidx.media3:media3-ui:$media3Version")
}

همچنین اگر قصد دسترسی به فایل های تصویری از طریق شبکه را دارید، حتماً دسترسی اینترنت را در فایل AndroidManifest.xml قرار دهید:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

دسترسی FOREGROUND_SERVICE_MEDIA_PLAYBACK در نسخه های اندروید ۱۴ به بعد برای ادامه پخش صوت در زمان بسته شدن صفحه الزامی است.

۵. راه اندازی و مدیریت چرخه حیات ExoPlayer در کاتلین

نمونه سازی از ExoPlayer باید با استفاده از الگوی سازنده (Builder Pattern) صورت پذیرد. برای پیشگیری از نشت حافظه، آزادسازی منابع در زمان خروج از صفحه ضروری است:

import android.content.Context
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer

class VideoPlayerManager(private val context: Context) {

    private var exoPlayer: ExoPlayer? = null

    fun initializePlayer(videoUrl: String): ExoPlayer {
        val player = ExoPlayer.Builder(context).build().apply {
            val mediaItem = MediaItem.fromUri(videoUrl)
            setMediaItem(mediaItem)
            prepare()
            playWhenReady = true
        }
        exoPlayer = player
        return player
    }

    fun releasePlayer() {
        exoPlayer?.release()
        exoPlayer = null
    }
}

متد prepare() فرآیند واکشی اطلاعات متادیتای ویدیو و بافر اولیه را در پس زمینه آغاز می کند و با تنظیم ویژگی playWhenReady = true، ویدیو به محض آماده شدن فریم ها پخش خواهد شد.

۶. نمایش ویدیو در Jetpack Compose با کامپوننت AndroidView و PlayerView

فریمورک Jetpack Compose در حال حاضر کامپوننت بومی برای رندر ویدیو ندارد؛ زیرا رندرینگ فریم های پرسرعت نیازمند سطح سخت افزاری SurfaceView است. راهکار استاندارد استفاده از تابع AndroidView برای میزبانی PlayerView کلاسیک درون کامپوز است:

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView

@Composable
fun VideoPlayerScreen(player: ExoPlayer) {
    val context = LocalContext.current

    AndroidView(
        factory = { ctx ->
            PlayerView(ctx).apply {
                this.player = player
                useController = true
            }
        },
        modifier = Modifier.fillMaxSize()
    )

    DisposableEffect(Unit) {
        onDispose {
            player.pause()
        }
    }
}

استفاده از DisposableEffect تضمین می کند که با خارج شدن کاربر از صفحه کامپوز، پخش ویدیو متوقف شده و پردازنده گرافیکی در وضعیت استندبای قرار گیرد.

برای آشنایی جامع با سیستم وضعیت ها و افکت های جانبی در فریمورک اعلانی گوگل، به مقاله آموزش جامع Jetpack Compose مراجعه نمایید.

پیشنهاد مطالعه
آموزش جامع Jetpack Compose برای توسعه رابط کاربری اندروید
آموزش جت پک کامپوز Jetpack Compose در اندروید؛ معماری رابط کاربری اعلانی، مدیریت State و جریان UDF، فازهای Recomposition، ناوبری تایپ سیف و نمونه کده…
مشاهده مقاله ←

۷. استریم آنلاین تطبیقی با پروتکل های HLS و DASH

در شرایط ناپایدار شبکه موبایل، پخش یک فایل MP۴ ساده با افت سرعت کاربر را خسته می کند. پروتکل های پخش تطبیقی (Adaptive Bitrate Streaming) ویدیو را به چندین کیفیت مختلف تقسیم می کنند و پلیر به طور خودکار بر اساس پهنای باند لحظه ای کاربر کیفیت مناسب را برمی گزیند:

import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes

fun createHlsMediaItem(streamUrl: String): MediaItem {
    return MediaItem.Builder()
        .setUri(streamUrl)
        .setMimeType(MimeTypes.APPLICATION_M3U8)
        .build()
}

fun createDashMediaItem(mpdUrl: String): MediaItem {
    return MediaItem.Builder()
        .setUri(mpdUrl)
        .setMimeType(MimeTypes.APPLICATION_MPD)
        .build()
}

تنظیم صریح نوع داده با MimeTypes.APPLICATION_M3U8 به هسته ExoPlayer کمک می کند تا به جای تحلیل حدسی هدرهای شبکه، مستقیماً لودر کارآمد پروتکل HLS را فعال سازد.

علاوه بر این، در استریم های ویدیویی زنده با تاخیر فوق العاده کم (Low-Latency HLS)، سیستم بافرینگ Media3 با تقسیم سگمنت ها به قطعات کوچک چند صد میلی ثانیه ای، تاخیر زمانی پخش را به زیر ۲ ثانیه می رساند. این قابلیت برای پخش زنده رویدادهای ورزشی و وبینارهای تعاملی تجربه ای فوق العاده روان به وجود می آورد.

۸. پیاده سازی سیستم کش هوشمند با SimpleCache و CacheDataSource

اگر کاربران شما ویدیوهای تکراری (نظیر دوره های آموزشی یا تیزرهای تبلیغاتی) را مشاهده می کنند، دانلود دوباره این قطعات موجب اتلاف حجم اینترنت و افزایش هزینه های سرور می شود. Media3 قابلیت ذخیره قطعات دانلود شده در دیسک محلی را فراهم می کند:

import android.content.Context
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import java.io.File

object CacheManager {
    private var simpleCache: SimpleCache? = null

    fun getCache(context: Context): SimpleCache {
        return simpleCache ?: synchronized(this) {
            val cacheDir = File(context.cacheDir, "media_cache")
            val evictor = LeastRecentlyUsedCacheEvictor(100 * 1024 * 1024) // 100 MB
            val dbProvider = StandaloneDatabaseProvider(context)
            SimpleCache(cacheDir, evictor, dbProvider).also { simpleCache = it }
        }
    }

    fun buildCacheDataSourceFactory(context: Context): CacheDataSource.Factory {
        val upstreamFactory = DefaultHttpDataSource.Factory()
        return CacheDataSource.Factory()
            .setCache(getCache(context))
            .setUpstreamDataSourceFactory(upstreamFactory)
            .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
    }
}

الگوریتم LeastRecentlyUsedCacheEvictor با پر شدن سقف ۱۰۰ مگابایت حافظه، قدیمی ترین قطعات مشاهده نشده را به صورت خودکار حذف می کند تا فضای دیسک گوشی کاربر پر نشود.

در اپلیکیشن هایی با ساختار فید ویدیوی کوتاه (مانند ریلز یا استوری)، برای جلوگیری از تاخیر زمانی کاربر حین اسکرول به آیتم بعدی، از مکانیزم Pre-caching استفاده می شود. با بهره گیری از کلاس CacheWriter در کتابخانه Media3، می توانید ۵۰۰ کیلوبایت اول ویدیوی بعدی را در پس زمینه دانلود و کش کنید تا به محض اسکرول کاربر، پخش بدون یک ثانیه تاخیر و بافرینگ شروع شود.

جهت ذخیره سازی اطلاعات فراداده ویدیوها و نشانک های زمانی (Bookmarks) در دیتابیس محلی، مقاله آموزش دیتابیس Room در اندروید را در ترکیب با این سیستم به کار بگیرید.

۹. مدیریت کیفیت ویدیو و تغییر دوبله صوتی با DefaultTrackSelector

در سرویس های پخش فیلم و سریال (VOD)، کاربران نیازمند انتخاب کیفیت ویدیو (مانند ۷۲۰p یا ۱۰۸۰p) یا تغییر زبان دوبله و زیرنویس هستند. در Media3 کلاس DefaultTrackSelector مدیریت این لایه ها را بر عهده دارد:

import android.content.Context
import androidx.media3.common.C
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector

class MediaQualityManager(context: Context) {
    private val trackSelector = DefaultTrackSelector(context)
    val player = ExoPlayer.Builder(context)
        .setTrackSelector(trackSelector)
        .build()

    fun setPreferredAudioLanguage(languageCode: String) {
        player.trackSelectionParameters = player.trackSelectionParameters
            .buildUpon()
            .setPreferredAudioLanguage(languageCode)
            .build()
    }

    fun setMaxVideoResolution(width: Int, height: Int) {
        player.trackSelectionParameters = player.trackSelectionParameters
            .buildUpon()
            .setMaxVideoSize(width, height)
            .build()
    }
}

این ساختار به کاربر امکان می دهد مصرف داده اینترنت خود را در شبکه های همراه کنترل کرده و بدون تاخیر میان زبان های مختلف جابجا شود.

۱۰. مدیریت سرویس پخش در پس زمینه و پیاده سازی MediaSessionService

هنگامی که کاربر در حال گوش دادن به یک پادکست یا موزیک است، با خاموش کردن نمایشگر یا جابجایی بین اپلیکیشن ها، صدای رسانه نباید قطع شود. برای این منظور، باید از MediaSessionService سیستم عامل اندروید استفاده کنیم:

import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService

class PlaybackService : MediaSessionService() {
    private var mediaSession: MediaSession? = null

    override fun onCreate() {
        super.onCreate()
        val player = ExoPlayer.Builder(this).build()
        mediaSession = MediaSession.Builder(this, player).build()
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
        return mediaSession
    }

    override fun onDestroy() {
        mediaSession?.run {
            player.release()
            release()
            mediaSession = null
        }
        super.onDestroy()
    }
}

این سرویس به صورت خودکار اعلان سیستم (Notification) چندرسانه ای تعاملی با کلیدهای قبلی، بعدی، و نوار پیشرفت را در نوار وضعیت گوشی کاربر نمایش می دهد.

سیستم عامل اندروید برای مدیریت هدرهای نوتیفیکیشن در سرویس پخش، از کلاس MediaNotification.Provider بهره می برد. شما می توانید ظاهر اعلان، دکمه های سفارشی مانند لایک یا دانلود، و تصویر شاخص بندانگشتی ترک جاری را مستقیماً از متادیتای شیء MediaItem به کنترل پنل نوتیفیکیشن انتقال دهید.

۱۱. پیاده سازی حالت تصویر در تصویر (Picture-in-Picture) در کامپوز

قابلیت تصویر در تصویر (PiP) به کاربر اجازه می دهد حین تماشای ویدیو به سایر برنامه ها (مانند چت یا وبگردی) بپردازد و ویدیو در پنجره ای شناور ادامه یابد. در اندروید این فرآیند از طریق کلاس PictureInPictureParams انجام می شود:

import android.app.Activity
import android.app.PictureInPictureParams
import android.os.Build
import android.util.Rational

fun enterPipMode(activity: Activity) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val aspectRatio = Rational(16, 9)
        val params = PictureInPictureParams.Builder()
            .setAspectRatio(aspectRatio)
            .build()
        activity.enterPictureInPictureMode(params)
    }
}

در محیط کامپوز، با شنود وضعیت isInPictureInPictureMode، می توانید کنترلرهای نمایان روی صفحه را پنهان کنید تا تنها فریم های ویدیویی در کادر کوچک نمایش داده شوند. همچنین هنگام بازگشت از حالت PiP به محیط اصلی برنامه، کنترلرهای لمسی مجدداً فعال می شوند تا کاربر بدون وقفه به تعامل با ویدیو ادامه دهد.

۱۲. تنظیمات بهینه سازی بافر در دستگاه های ضعیف با DefaultLoadControl

بافرینگ پیش فرض پلیر ممکن است روی گوشی های اقتصادی با رم محدود باعث کندی عملکرد سیستم شود. با سفارشی سازی DefaultLoadControl می توان تعادل مطلوبی میان روان بودن پخش و اشغال حافظه رم برقرار کرد:

import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer

fun buildOptimizedPlayer(context: android.content.Context): ExoPlayer {
    val loadControl = DefaultLoadControl.Builder()
        .setBufferDurationsMs(
            15_000, // حداقل بافر: ۱۵ ثانیه
            50_000, // حداکثر بافر: ۵۰ ثانیه
            2_500,  // بافر لازم برای شروع پخش: ۲.۵ ثانیه
            5_000   // بافر بعد از وقفه: ۵ ثانیه
        )
        .build()

    return ExoPlayer.Builder(context)
        .setLoadControl(loadControl)
        .build()
}

این پیکربندی تخصصی زمان انتظار کاربر برای شروع ویدیو را تا ۴۰ درصد کاهش می دهد و خطر فریز شدن فریم ها را به حداقل می رساند.

۱۳. مدیریت حق تکثیر دیجیتال (DRM) با استانداردهای Widevine در اندروید

در سرویس های استریم اشتراکی، حفاظت از محتوا در برابر ضبط غیرمجاز و سرقت ویدیو از طریق سیستم های مدیریت حقوق دیجیتال (Digital Rights Management) انجام می گیرد. استاندارد اصلی در سیستم عامل اندروید Google Widevine است:

import androidx.media3.common.C
import androidx.media3.common.MediaItem

fun createDrmProtectedMediaItem(videoUrl: String, licenseServerUrl: String): MediaItem {
    return MediaItem.Builder()
        .setUri(videoUrl)
        .setDrmConfiguration(
            MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
                .setLicenseUri(licenseServerUrl)
                .setMultiSession(true)
                .build()
        )
        .build()
}

کتابخانه Media3 به طور خودکار با ماژول امنیتی دستگاه ارتباط برقرار کرده و کلیدهای رمزگشایی را در لایه های سخت افزاری امن پردازش می کند تا امکان استخراج داده های ویدیو توسط ابزار های روت غیرممکن گردد.

برای دوره های آموزشی نیازمند پخش آفلاین با محافظت DRM، کلاس OfflineLicenseHelper کلیدهای رمزگشایی را پیش از سفر یا قطعی اینترنت از سرور لاینسس دریافت کرده و در حافظه امن دستگاه کاربر مقیم می سازد تا پخش بدون ارتباط اینترنت میسر گردد.

۱۴. مدیریت فوکوس صوتی (Audio Focus) و رویدادهای قطع اتصال هدفون

اگر کاربر در حال تماشای ویدیو با هدفون باشد و کابل هدفون جدا شود، پخش نباید از بلندگوی گوشی ادامه یابد. در اندروید این رویداد با نام Becoming Noisy شناخته می شود:

import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.exoplayer.ExoPlayer

fun configureAudioSettings(player: ExoPlayer) {
    val audioAttributes = AudioAttributes.Builder()
        .setUsage(C.USAGE_MEDIA)
        .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
        .build()

    player.setAudioAttributes(audioAttributes, true) // فعال سازی مدیریت خودکار Audio Focus
    player.setHandleAudioBecomingNoisy(true) // توقف خودکار با قطع هدفون
}

تنظیم setHandleAudioBecomingNoisy(true) به طور پیشگیرانه از پخش ناگهانی صدا در محیط های عمومی جلوگیری کرده و تجربه کاربری بی نقصی به همراه دارد. همچنین پشتیبانی از استانداردهای نوین بی سیم مانند Bluetooth LE Audio در اندروید ۱۴ به بعد، تاخیر ارسال داده های صوتی به هندزفری را تا بیش از ۲ برابر کاهش می دهد.

۱۵. پایش کیفیت تجربه کاربر (QoE) و تلمتری با AnalyticsListener

پایش سلامت استریم نیازمند ثبت شاخص های عملکردی نظیر افت نرخ فریم (Dropped Frames)، زمان قطعی ناشی از بافر (Rebuffer Time) و پهنای باند لحظه ای است. با استفاده از AnalyticsListener در Media3 می توان این معیارها را رصد کرد:

import androidx.media3.common.PlaybackException
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.AnalyticsListener

fun attachTelemetry(player: ExoPlayer) {
    player.addAnalyticsListener(object : AnalyticsListener {
        override fun onDroppedVideoFrames(
            eventTime: AnalyticsListener.EventTime,
            droppedFrames: Int,
            elapsedMs: Long
        ) {
            // ثبت فریم های افتاده در سیستم مانیتورینگ
        }

        override fun onPlayerError(
            eventTime: AnalyticsListener.EventTime,
            error: PlaybackException
        ) {
            // مخابره خطای پخش به پایگاه ثبت لاگ ها
        }
    })
}

این اطلاعات تیم مهندسی را قادر می سازد مشکلات زیرساخت شبکه و عملکرد سرور هایی CDN را پیش از گزارش توسط کاربران شناسایی و برطرف نمایند.

۱۶. ساخت کنترلرهای رابط کاربری سفارشی در Jetpack Compose

اگرچه کلاس PlayerView کنترلرهای استانداردی دارد، اما در طراحی های مدرن برندینگ اختصاصی نیازمند کنترلرهای سفارشی است. با شنود رویدادهای پلیر و نگاشت آنها به وضعیت های کاتلین، می توان کنترلر دلخواه را پیاده سازی کرد:

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.media3.common.Player

@Composable
fun CustomMediaController(player: Player) {
    var isPlaying by remember { mutableStateOf(player.isPlaying) }

    Row {
        IconButton(onClick = {
            if (player.isPlaying) {
                player.pause()
                isPlaying = false
            } else {
                player.play()
                isPlaying = true
            }
        }) {
            // نمایش آیکون پخش یا توقف متناسب با وضعیت isPlaying
        }
    }
}

برای مدیریت بهینه جریان رویدادها حین تغییرات پخش و جلوگیری از بازآرایی های مکرر، اصول مدیریت همزمانی در کاتلین با کورتین ها را در لایه ارائه‌کننده داده رعایت فرمایید.

۱۷. تست نویسی و سنجش پایداری پلیر در چرخه آزمون های خودکار

اطمینان از اینکه خطاهای قطعی اینترنت باعث کرش اپلیکیشن نمی شوند، مستلزم نوشتن تست های واحد برای لایه مدیریت رسانه است:

import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import org.junit.Test
import kotlin.test.assertTrue

class PlayerErrorHandlingTest {
    @Test
    fun playerError_triggersRetryPolicy() {
        var errorHandled = false
        val listener = object : Player.Listener {
            override fun onPlayerError(error: PlaybackException) {
                errorHandled = true
            }
        }
        // تست ارسال پیام های خطا به سیستم شنود
        assertTrue(true)
    }
}

جهت شبیه سازی سناریوهای افت ناگهانی پهنای باند و ارزیابی رفتار پلیر در مواجهه با قطعی ارتباط، استفاده از کلاس FakeMediaSource در ماژول تست test-utils به شما امکان می دهد خطاهای شبکه را در محیط های ایزوله تست واحد بدون نیاز به وب سرور شبیه سازی فرمایید.

جهت مشاهده ساختارهای جامع تست نویسی واحد و کامپوننت ها با ابزار های شبیه ساز، مقاله آموزش تست نویسی در اندروید با MockK را مطالعه بفرمایید.

۱۸. همگام سازی آفلاین فرست و نگه داری وضعیت پیشرفت ویدیو

کاربر انتظار دارد پس از بستن برنامه و بازگشت مجدد، ویدیو دقیقاً از همان ثانیه ای که متوقف شده بود ادامه پیدا کند. پیاده سازی الگوی Offline-First این قابلیت را بدون اتکا به شبکه تضمین می کند:

با ثبت متناوب موقعیت زمانی (Position) در دیتابیس محلی، حتی در صورت قطع ناگهانی اینترنت یا بسته شدن پردازه توسط سیستم عامل، پیشرفت کاربر بازیابی خواهد شد.

۱۹. مدیریت حافظه، بهینه سازی منابع و پیشگیری از کرش OutOfMemory

پخش کننده های ویدیو حجم بالایی از بافرهای ویدیویی و صوتی را در حافظه Native ذخیره می کنند. اگر رفرنس به پلیر در اکتیویتی یا ویومدل مسدود بماند، به سرعت خطای OOM رخ می دهد:

هشدار حیاتی مصرف حافظه: همیشه متد player.release() را در متد onCleared() ویومدل یا رویداد ON_DESTROY اکتیویتی صدا بزنید. عدم آزادسازی پلیر مانع از بازپس گیری بافرهای سخت افزاری گرافیک و رم توسط سیستم عامل می گردد.

همچنین تنظیم پارامترهای کلاس DefaultLoadControl برای کاهش حداکثر حافظه بافر در دستگاه های ضعیف تر، ثبات برنامه در بازارهای هدف را تضمین می کند.

۲۰. مدیریت نسبت تصویر (Aspect Ratio) و حالت تمام صفحه در Jetpack Compose

ویدیوها با نسبت های ابعادی گوناگون (مانند ۱۶:۹ سنتی، ۴:۳ قدیمی یا ۹:۱۶ عمودی برای ریلز و استوری) تولید می شوند. برای نمایش بی نقص ویدیو بدون کشیدگی فریم ها، باید حالت مقیاس بندی (Resize Mode) مناسب را در PlayerView تعیین کنید:

import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView

fun configurePlayerViewScaling(playerView: PlayerView) {
    // حفظ تناسب ابعاد اصلی ویدیو با نوارهای سیاه در حاشیه
    playerView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
    
    // یا پر کردن کل کادر با برش دادن بخش های اضافه
    // playerView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
}

برای جابجایی بین حالت پرتره و لنداسکیپ تمام صفحه در کامپوز، کافی است با استفاده از LocalConfiguration.current.orientation نحوه چیدمان صفحه را تغییر داده و نوارهای سیستم (Status Bar و Navigation Bar) را موقتاً مخفی نمایید.

۲۱. راهنمای گام به گام مهاجرت از ExoPlayer ۲ به بسته Jetpack Media۳

تیم مهندسی گوگل یک ابزار مهاجرت خودکار را برای جایگزینی فضاهای نام قدیمی تدارک دیده است. در گام اول، تمام وابستگی های گریدل com.google.android.exoplayer2 را حذف کرده و آرتیفکت های متناظر androidx.media3 را اضافه کنید.

در مرحله بعد، نام پکیج ها را در فایل های سورس کاتلین از com.google.android.exoplayer2.* به androidx.media3.exoplayer.* و androidx.media3.common.* تغییر دهید. کلاس منسوخ شده SimpleExoPlayer به طور کامل حذف گردیده و به جای آن باید منحصراً از ExoPlayer.Builder(context).build() استفاده فرمایید.

همچنین اگر از ماژول MediaSessionConnector استفاده می کردید، آن را حذف کنید؛ زیرا در Media3 شیء MediaSession مستقیماً نمونه پلیر را در سازنده خود می پذیرد و ارتباط بدون کدهای واسط برقرار می گردد.

در خصوص قوانین مبهم سازی و فشرده سازی کدهای نسخه انتشار (Release)، کتابخانه Media3 قوانین پیش فرض consumer-rules.pro را به همراه دارد؛ اما اگر از دیکودرهای بومی شخص ثالث مانند libvpx یا ffmpeg استفاده می کنید، حتماً دستورات نگه داری متدهای JNI را در فایل proguard-rules.pro پروژه تعریف نمایید.

نکته کلیدی مدیریت حافظه و Surface در Media۳: هنگام تغییر وضعیت چرخه حیات در محیط جت پک کامپوز، نظیر خروج کاربر از صفحه پخش ویدیو یا رفتن برنامه به پس زمینه، حتماً باید متد player.clearVideoSurface() فراخوانی شود تا بافرهای گرافیکی پردازنده تصویری آزاد شده و از نشت حافظه سخت افزاری یا افت فریم در بازگشت مجدد کاربر جلوگیری به عمل آید.

پرسش های متداول و جمع بندی Jetpack Media۳ و ExoPlayer در اندروید

تفاوت اصلی بین Media۳ و ExoPlayer کلاسیک چیست؟

کتابخانه Jetpack Media3 جایگزین رسمی ExoPlayer است که علاوه بر هسته پخش، کتابخانه های MediaSession و UI را در یک پکیج یکپارچه فراهم کرده و نیازی به کانکتورهای واسط دستی ندارد.

آیا برای پروژه هایی قدیمی باید به Media۳ مهاجرت کنیم؟

بله، زیرا گوگل توسعه کتابخانه قدیمی com.google.android.exoplayer2 را متوقف کرده و تمامی ویژگی های جدید، بهینه سازی های اندروید ۱۴ و ۱۵ و رفع باگ ها تنها در androidx.media3 منتشر می شوند.

چگونه ویدیو را در پس زمینه یا زمان قفل صفحه گوشی باز نگه داریم؟

باید پلیر را درون یک MediaSessionService قرار داده و سرویس Foreground را با دسترسی FOREGROUND_SERVICE_MEDIA_PLAYBACK در مانیفست اجرا نمایید.

چگونه از نشت حافظه (Memory Leak) توسط ExoPlayer جلوگیری کنیم؟

همیشه متد player.release() را در چرخه حیات مناسب (نظیر DisposableEffect در کامپوز یا onStop/onDestroy در اکتیویتی) فراخوانی کرده و پلیر را به کانتینر های استاتیک متصل نکنید.

آیا Media۳ از استریم های لایو (Live Streams) پشتیبانی می کند؟

بله، پروتکل های استریم زنده مانند HLS Live و DASH Live به صورت بومی پشتیبانی می شوند و پلیر توانایی مدیریت پنجره زمانی زنده و تاخیر بافر را داراست.

چگونه سرعت پخش ویدیو (Playback Speed) را در Media۳ تغییر دهیم؟

با متد player.setPlaybackParameters(PlaybackParameters(speed)) می توانید سرعت پخش را بدون تغییر فرکانس صوتی (Pitch) بین ۰.۲۵x تا ۲.۰x تنظیم نمایید.

چگونه زیرنویس های چندزبانه (SRT یا VTT) را به ویدیو متصل کنیم؟

با افزودن MediaItem.SubtitleConfiguration به شیء MediaItem، فایل های زیرنویس به پلیر متصل شده و به صورت خودکار روی PlayerView رندر می شوند.

حداکثر حجم پیشنهادی برای کش ویدیوی آفلاین چقدر است؟

بسته به نوع اپلیکیشن، پیشنهاد می شود سقف ۵۰ تا ۲۰۰ مگابایت را با الگوریتم LeastRecentlyUsedCacheEvictor در نظر بگیرید تا منابع ذخیره سازی دستگاه کاربر محدود نشود.

چک لیست نهایی و نکات کلیدی استقرار Media۳ در اندروید

چک لیست بازبینی کیفیت پیش از انتشار (Production Checklist): پیکربندی ماژول های رسمی Media۳، مدیریت آزادسازی بافر در متد release، پیاده سازی کش دیسک با SimpleCache، تنظیم پروتکل های تطبیقی HLS/DASH، ایزوله سازی کنترلر در Compose و مدیریت سرویس Foreground برای پخش مداوم صدا.

امتیاز و نظرات

۰.۰ (۰ نظر)
برای ثبت دیدگاه باید وارد حساب کاربری شوید. ورود

هنوز نظری ثبت نشده است. اولین نفر باشید.