Core Compose

media3-ui-compose लाइब्रेरी, Jetpack Compose में मीडिया यूज़र इंटरफ़ेस बनाने के लिए बुनियादी कॉम्पोनेंट उपलब्ध कराती है. इसे उन डेवलपर के लिए बनाया गया है जिन्हें media3-ui-compose-material3 लाइब्रेरी के मुकाबले, ज़्यादा बदलाव करने की ज़रूरत होती है. इस पेज पर, कस्टम मीडिया प्लेयर यूज़र इंटरफ़ेस (यूआई) बनाने के लिए, मुख्य कॉम्पोनेंट और स्टेट होल्डर इस्तेमाल करने का तरीका बताया गया है.

Material3 और कस्टम Compose कॉम्पोनेंट को एक साथ इस्तेमाल करना

media3-ui-compose-material3 लाइब्रेरी को इस तरह डिज़ाइन किया गया है कि इसे ज़रूरत के हिसाब से इस्तेमाल किया जा सके. अपने ज़्यादातर यूज़र इंटरफ़ेस (यूआई) के लिए, पहले से बने कॉम्पोनेंट का इस्तेमाल किया जा सकता है. हालांकि, ज़्यादा कंट्रोल की ज़रूरत होने पर, कस्टम तरीके से लागू करने के लिए किसी एक कॉम्पोनेंट को बदला जा सकता है. ऐसे में, media3-ui-compose लाइब्रेरी काम आती है.

उदाहरण के लिए, मान लें कि आपको Material3 लाइब्रेरी से स्टैंडर्ड PreviousButton और NextButton का इस्तेमाल करना है, लेकिन आपको पूरी तरह से कस्टम PlayPauseButton की ज़रूरत है. इसके लिए, कोर media3-ui-compose लाइब्रेरी से PlayPauseButton का इस्तेमाल करें और इसे पहले से मौजूद कॉम्पोनेंट के साथ रखें.

Row {
  // Use prebuilt component from the Media3 UI Compose Material3 library
  PreviousButton(player)
  // Use the scaffold component from Media3 UI Compose library
  PlayPauseButton(player) {
    // `this` is PlayPauseButtonState
    FilledTonalButton(
      onClick = {
        Log.d("PlayPauseButton", "Clicking on play-pause button")
        this.onClick()
      },
      enabled = this.isEnabled,
    ) {
      Icon(
        imageVector = if (showPlay) Icons.Default.PlayArrow else Icons.Default.Pause,
        contentDescription = if (showPlay) "Play" else "Pause",
      )
    }
  }
  // Use prebuilt component from the Media3 UI Compose Material3 library
  NextButton(player)
}

उपलब्ध कॉम्पोनेंट

media3-ui-compose लाइब्रेरी, सामान्य प्लेयर कंट्रोल के लिए पहले से बने कंपोज़ेबल का सेट उपलब्ध कराती है. यहां कुछ कॉम्पोनेंट दिए गए हैं जिन्हें सीधे अपने ऐप्लिकेशन में इस्तेमाल किया जा सकता है:

कॉम्पोनेंट ब्यौरा
PlayPauseButton यह एक स्टेट कंटेनर है. इसका इस्तेमाल, मीडिया चलाने और रोकने के बीच टॉगल करने वाले बटन के लिए किया जाता है.
SeekBackButton यह एक स्टेट कंटेनर है. इसका इस्तेमाल, तय की गई इंक्रीमेंट वैल्यू के हिसाब से पीछे ले जाने वाले बटन के लिए किया जाता है.
SeekForwardButton यह एक स्टेट कंटेनर है. इसका इस्तेमाल, तय की गई इंक्रीमेंट वैल्यू के हिसाब से आगे बढ़ाने वाले बटन के लिए किया जाता है.
NextButton यह एक स्टेट कंटेनर है. इसका इस्तेमाल, अगले मीडिया आइटम पर जाने वाले बटन के लिए किया जाता है.
PreviousButton यह एक स्टेट कंटेनर है. इसका इस्तेमाल, पिछले मीडिया आइटम पर जाने वाले बटन के लिए किया जाता है.
RepeatButton यह उस बटन के लिए एक स्टेट कंटेनर है जो रिपीट मोड के बीच साइकल करता है.
ShuffleButton यह एक स्टेट कंटेनर है. इसका इस्तेमाल, शफ़ल मोड को टॉगल करने वाले बटन के लिए किया जाता है.
MuteButton यह एक स्टेट कंटेनर है. इसका इस्तेमाल, प्लेयर को म्यूट और अनम्यूट करने वाले बटन के लिए किया जाता है.
TimeText यह एक स्टेट कंटेनर है. इसका इस्तेमाल, प्लेयर की प्रोग्रेस दिखाने वाले कंपोज़ेबल के लिए किया जाता है.
ContentFrame मीडिया कॉन्टेंट दिखाने के लिए एक प्लैटफ़ॉर्म, जो आसपेक्ट रेशियो (लंबाई-चौड़ाई का अनुपात) को मैनेज करने, साइज़ बदलने, और शटर को मैनेज करने की सुविधा देता है
PlayerSurface रॉ प्लैटफ़ॉर्म, जो AndroidView में SurfaceView और TextureView को रैप करता है.

Player कंपोज़ेबल को पसंद के मुताबिक बनाना

media3-ui-compose-material3 लाइब्रेरी का इस्तेमाल करने पर, Player कंपोज़ेबल, मीडिया प्लेबैक का पूरा अनुभव देता है. इसके लेआउट को पसंद के मुताबिक बनाने के लिए, इसके कॉन्टेंट स्लॉट में अपने कंपोज़ेबल उपलब्ध कराएं. इसके अलावा, यूज़र इंटरफ़ेस (यूआई) के कुछ हिस्सों को पसंद के मुताबिक बनाने के लिए, PlayerDefaults ऑब्जेक्ट का इस्तेमाल करें. जैसे, TopControls, CenterControls, BottomControls, और ErrorOverlay.

पहली इमेज में दिखाया गया है कि Player कंपोज़ेबल, अपने स्लॉट कहां रखता है. दूसरी इमेज में, लेयर बनाने का क्रम दिखाया गया है.

डैश वाली आउटलाइन वाला वीडियो प्लेयर, जो इसके स्लॉट को मार्क करता है. सबसे ऊपर से सबसे नीचे तक: सबसे ऊपर के किनारे पर topControls, बीच के ऊपर errorOverlay, backSecondary, back, central, forward, और forwardSecondary बटन के साथ बीच में centerControls, सबसे नीचे के पास subtitles, और ऊपर, बाएं, progressSlider, दाएं, और नीचे के स्लॉट के साथ सबसे नीचे के किनारे पर bottomControls. ContentFrame, उनके पीछे की जगह को भरता है.
पहली इमेज. Player कंपोज़ेबल के स्लॉट. बीच और सबसे नीचे मौजूद कंट्रोल, PlayerDefaults.CenterControls और PlayerDefaults.BottomControls के स्लॉट दिखाते हैं. topControls, above, और below स्लॉट, डिफ़ॉल्ट रूप से खाली होते हैं.
स्टैक की गई छह लेयर का एक्सप्लोडेड व्यू. नीचे से ऊपर की ओर: 1, PlayerSurface; 2, शटर; 3, आर्टवर्क; 4, सबटाइटल; 5, errorOverlay; और 6, topControls, centerControls, और bottomControls. लेयर 1 से 4 तक, ContentFrame बनाती हैं. वहीं, सभी छह लेयर मिलकर Player बनाती हैं.
दूसरी इमेज. Player कंपोज़ेबल की लेयर. इन्हें ड्रॉ किए जाने के क्रम में नंबर दिए गए हैं.

उदाहरण के लिए, PlayerDefaults.CenterControls का इस्तेमाल करके, बीच में मौजूद कंट्रोल को बिना बदले, सिर्फ़ बीच में मौजूद वीडियो चलाने/रोकने वाले बटन को बदला जा सकता है. आपके पास topControls जैसे पैरामीटर के लिए, पूरी तरह से कस्टम कंपोज़ेबल पास करने का विकल्प भी होता है. साथ ही, bottomControls जैसे अन्य पैरामीटर को पूरी तरह से छोड़ा जा सकता है, ताकि उन्हें डिफ़ॉल्ट पर सेट किया जा सके.

@Composable
fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) {
  Player(
    player = player,
    modifier = modifier,
    showControls = showControls,
    topControls = {
      // Fully custom top controls
      Text("My custom title")
    },
    centerControls = {
      // Use default CenterControls but override the central button
      PlayerDefaults.CenterControls(
        player = player,
        central = {
          // A custom play/pause button
          Material3PlayPauseButton(player, modifier = Modifier.size(64.dp))
        },
      )
    },
    // bottomControls are left as default
  )
}

यूज़र इंटरफ़ेस (यूआई) स्टेट होल्डर

अगर कोई भी स्कैफ़ोल्ड कॉम्पोनेंट आपकी ज़रूरत के मुताबिक नहीं है, तो सीधे तौर पर स्टेट ऑब्जेक्ट का भी इस्तेमाल किया जा सकता है. आम तौर पर, हमारा सुझाव है कि रीकंपोज़िशन के बीच यूज़र इंटरफ़ेस (यूआई) के लुक को बनाए रखने के लिए, उससे जुड़े remember तरीकों का इस्तेमाल करें.

यूज़र इंटरफ़ेस (यूआई) के स्टेट होल्डर और कंपोज़ेबल के इस्तेमाल के बीच के अंतर को बेहतर तरीके से समझने के लिए, Compose में स्टेट मैनेज करने के बारे में पढ़ें.

बटन के स्टेट होल्डर

यूआई की कुछ स्थितियों के लिए, लाइब्रेरी यह मान लेती है कि उन्हें बटन जैसे कॉम्पोज़ेबल इस्तेमाल करेंगे.

राज्य remember*State प्रकार
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState कॉन्स्टेंट
NextButtonState rememberNextButtonState कॉन्स्टेंट
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState मेन्यू या एन-टॉगल

PlayPauseButtonState के इस्तेमाल का उदाहरण:

val state = rememberPlayPauseButtonState(player)

IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
  Icon(
    imageVector = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause,
    contentDescription =
      if (state.showPlay) stringResource(R.string.playpause_button_play)
      else stringResource(R.string.playpause_button_pause),
  )
}

विज़ुअल आउटपुट के स्टेट होल्डर

CurrentMediaItemState, फ़िलहाल चल रहे MediaItem के बारे में जानकारी देता है. वहीं, PlaylistState, प्लेयर पर सेट किए गए MediaItems के बारे में जानकारी देता है. ये आपके कस्टम यूज़र इंटरफ़ेस (यूआई) में मेटाडेटा की जानकारी दिखाने के लिए काम आते हैं.

ErrorState, प्लेयर की मौजूदा गड़बड़ी की स्थिति के बारे में जानकारी देता है. इसका इस्तेमाल, गड़बड़ी का ओवरले दिखाने के लिए किया जा सकता है.

PresentationState में यह जानकारी होती है कि PlayerSurface में वीडियो आउटपुट कब दिखाया जा सकता है या इसे प्लेसहोल्डर यूज़र इंटरफ़ेस (यूआई) एलिमेंट से कवर किया जाना चाहिए. ContentFrame composable, आसपेक्ट रेशियो (लंबाई-चौड़ाई का अनुपात) को मैनेज करने के साथ-साथ, उस प्लैटफ़ॉर्म पर शटर दिखाने का ध्यान रखता है जो अभी तैयार नहीं है.

@Composable
fun ContentFrame(
  player: Player?,
  modifier: Modifier = Modifier,
  surfaceType: @SurfaceType Int = SURFACE_TYPE_SURFACE_VIEW,
  contentScale: ContentScale = ContentScale.Fit,
  keepContentOnReset: Boolean = false,
  shutter: @Composable () -> Unit = { Box(Modifier.fillMaxSize().background(Color.Black)) },
) {
  val presentationState = rememberPresentationState(player, keepContentOnReset)
  val scaledModifier =
    modifier.resizeWithContentScale(contentScale, presentationState.videoAspectRatio)

  // Always leave PlayerSurface to be part of the Compose tree because it will be initialized in
  // the process. If this composable is guarded by some condition, it might never become visible
  // because the Player won't emit the relevant event, e.g. the first frame being ready.
  PlayerSurface(player, scaledModifier, surfaceType)

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    shutter()
  }
}

यहां, हम presentationState.videoAspectRatio का इस्तेमाल करके, Surface को चुने गए आसपेक्ट रेशियो (लंबाई-चौड़ाई का अनुपात) के हिसाब से स्केल कर सकते हैं. ज़्यादा टाइप के लिए, कॉन्टेंट स्केल देखें. साथ ही, presentationState.coverSurface का इस्तेमाल करके यह पता लगा सकते हैं कि प्लैटफ़ॉर्म दिखाने का सही समय कब नहीं है. इस मामले में, अपारदर्शी शटर को प्लैटफ़ॉर्म के ऊपर रखा जा सकता है. जब प्लैटफ़ॉर्म तैयार हो जाएगा, तब यह शटर गायब हो जाएगा. ContentFrame की मदद से, शटर को ट्रेलिंग लैम्डा के तौर पर पसंद के मुताबिक बनाया जा सकता है. हालांकि, डिफ़ॉल्ट रूप से यह काला @Composable Box होता है और पैरंट कंटेनर के साइज़ में दिखता है.

Flows कहां उपलब्ध है?

कई Android डेवलपर, लगातार बदलने वाले यूज़र इंटरफ़ेस (यूआई) डेटा को इकट्ठा करने के लिए, Kotlin Flow ऑब्जेक्ट का इस्तेमाल करते हैं. उदाहरण के लिए, हो सकता है कि आपको ऐसा Player.isPlaying फ़्लो चाहिए जिसे लाइफ़साइकल के हिसाब से collect किया जा सके. या फिर Player.eventsFlow जैसा कुछ, ताकि आपको Flow<Player.Events> मिल सके. इसे अपनी पसंद के मुताबिक filter किया जा सकता है.

हालांकि, Player यूज़र इंटरफ़ेस (यूआई) की स्थिति के लिए फ़्लो का इस्तेमाल करने के कुछ नुकसान हैं. डेटा ट्रांसफ़र एसिंक्रोनस तरीके से होता है. यह एक मुख्य समस्या है. हमारा मकसद, Player.Event और यूज़र इंटरफ़ेस (यूआई) पर इसके इस्तेमाल के बीच कम से कम देरी करना है. साथ ही, ऐसे यूज़र इंटरफ़ेस (यूआई) एलिमेंट नहीं दिखाने हैं जो Player के साथ सिंक नहीं हैं.

अन्य बातों में ये शामिल हैं:

  • सभी Player.Events वाला फ़्लो, सिंगल रिस्पॉन्सिबिलिटी प्रिंसिपल का पालन नहीं करेगा. हर उपभोक्ता को काम के इवेंट फ़िल्टर करने होंगे.
  • हर Player.Event के लिए फ़्लो बनाने के लिए, आपको उन्हें हर यूज़र इंटरफ़ेस (यूआई) एलिमेंट के लिए combine के साथ जोड़ना होगा. Player.Event और यूज़र इंटरफ़ेस (यूआई) एलिमेंट में बदलाव के बीच, मेनी-टू-मेनी मैपिंग होती है. combine का इस्तेमाल करने से, यूज़र इंटरफ़ेस (यूआई) ऐसी स्थितियों में पहुंच सकता है जो गैर-कानूनी हो सकती हैं.

कस्टम यूज़र इंटरफ़ेस (यूआई) स्टेट बनाना

अगर मौजूदा यूज़र इंटरफ़ेस (यूआई) स्थितियां आपकी ज़रूरतों को पूरा नहीं करती हैं, तो आपके पास पसंद के मुताबिक यूज़र इंटरफ़ेस (यूआई) स्थितियां जोड़ने का विकल्प होता है. पैटर्न कॉपी करने के लिए, मौजूदा स्थिति के सोर्स कोड को देखें. आम तौर पर, यूज़र इंटरफ़ेस (यूआई) स्टेट होल्डर वाली क्लास ये काम करती है:

  1. यह Player ऑब्जेक्ट लेता है.
  2. यह कोरूटीन का इस्तेमाल करके, Player की सदस्यता लेता है. ज़्यादा जानकारी के लिए, Player.listen पर जाएं.
  3. यह अपनी इंटरनल स्टेट को अपडेट करके, खास Player.Events का जवाब देता है.
  4. कारोबारी नियम से जुड़े ऐसे निर्देश स्वीकार करता है जिन्हें सही Player अपडेट में बदला जाएगा.
  5. इसे यूज़र इंटरफ़ेस (यूआई) ट्री में कई जगहों पर बनाया जा सकता है. साथ ही, यह प्लेयर की स्थिति को हमेशा एक जैसा बनाए रखेगा.
  6. यह Compose State फ़ील्ड को एक्सपोज़ करता है. इनका इस्तेमाल, कंपोज़ेबल के ज़रिए बदलावों के हिसाब से डाइनैमिक तौर पर जवाब देने के लिए किया जा सकता है.
  7. इसमें remember*State फ़ंक्शन होता है, ताकि कंपोज़िशन के बीच इंस्टेंस को याद रखा जा सके.

पर्दे के पीछे क्या होता है:

class SomeButtonState(private val player: Player) {
  var isEnabled by mutableStateOf(player.isCommandAvailable(COMMAND_ACTION_A))
    private set

  var someFieldValue by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

  suspend fun observe(): Nothing = player.listen { events ->
    if (events.containsAny(EVENT_B_CHANGED, EVENT_C_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
      someFieldValue = this.someField
      isEnabled = this.isCommandAvailable(COMMAND_ACTION_A)
    }
  }
}

अपने Player.Events पर प्रतिक्रिया देने के लिए, उन्हें Player.listen का इस्तेमाल करके पकड़ा जा सकता है. यह एक suspend fun है, जो आपको कोरूटीन की दुनिया में जाने और अनिश्चित समय तक Player.Events को सुनने की सुविधा देता है. Media3 में यूज़र इंटरफ़ेस (यूआई) की अलग-अलग स्थितियों को लागू करने से, डेवलपर को Player.Events के बारे में सीखने की ज़रूरत नहीं पड़ती.