কোর কম্পোজ

Jetpack Compose-এ মিডিয়া UI তৈরি করার জন্য media3-ui-compose লাইব্রেরি প্রাথমিক কম্পোনেন্ট প্রদান করে। এটি সেইসব ডেভেলপারদের জন্য ডিজাইন করা হয়েছে যাদের media3-ui-compose-material3 লাইব্রেরির মাধ্যমে অফার করা কাস্টমাইজেশনের চেয়ে আরও বেশি কাস্টমাইজেশন প্রয়োজন। এই পৃষ্ঠায় কীভাবে কাস্টম মিডিয়া প্লেয়ার UI তৈরি করতে কোর কম্পোনেন্ট ও স্টেট হোল্ডার ব্যবহার করতে হয় তা ব্যাখ্যা করা হয়েছে।

Material3 ও কাস্টম Compose কম্পোনেন্ট মিশিয়ে ব্যবহার করা

media3-ui-compose-material3 লাইব্রেরি নমনীয় হওয়ার জন্য ডিজাইন করা হয়েছে। আপনি নিজের বেশিরভাগ UI-এর জন্য আগে থেকে তৈরি কম্পোনেন্ট ব্যবহার করতে পারবেন, তবে আরও কন্ট্রোল প্রয়োজন হলে কাস্টম ইমপ্লিমেন্টেশনের জন্য একটি কম্পোনেন্ট বদল করে নিতে পারবেন। এই সময় 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 কম্পোজ করার উপযুক্ত আইটেম সম্পূর্ণ মিডিয়া প্লেব্যাক অভিজ্ঞতা প্রদান করে। এর কন্টেন্ট স্লটে আপনার নিজস্ব কম্পোজেবল প্রদান করে আপনি এর লেআউট কাস্টমাইজ করতে পারেন অথবা UI-এর নির্দিষ্ট অংশ কাস্টমাইজ করতে PlayerDefaults অবজেক্ট ব্যবহার করতে পারেন, যেমন TopControls, CenterControls, BottomControls এবং ErrorOverlay.

যেমন, আপনি PlayerDefaults.CenterControls ব্যবহার করে শুধু মাঝখানের প্লে/পজ বোতাম ওভাররাইড করতে পারেন, বাকি কন্ট্রোল অক্ষত থাকবে। এছাড়াও, আপনি topControls-এর মতো প্যারামিটারের জন্য সম্পূর্ণ কাস্টম কম্পোজ করার মতো আইটেম পাস করতে পারেন এবং bottomControls-এর মতো আইটেম সম্পূর্ণভাবে বাদ দিতে পারেন যাতে সেগুলি ডিফল্ট হিসেবেই থাকে।

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

UI স্টেট হোল্ডার

স্ক্যাফোল্ডিং কম্পোনেন্টগুলির মধ্যে কোনওটি আপনার প্রয়োজন পূরণ করতে না পারলে, আপনি সরাসরি স্টেট অবজেক্টও ব্যবহার করতে পারেন। সাধারণত, রিকম্পোজিশনের মধ্যে আপনার UI লুক বজায় রাখতে, সংশ্লিষ্ট remember পদ্ধতি ব্যবহার করার পরামর্শ দেওয়া হয়।

আপনি কীভাবে কম্পোজ করার যোগ্য আইটেমের তুলনায় UI স্টেট হোল্ডারদের নমনীয়তা ব্যবহার করতে পারবেন তা আরও ভালোভাবে বুঝতে, Compose কীভাবে স্টেট ম্যানেজ করে সেই সম্পর্কে পড়ুন।

বোতাম স্টেট হোল্ডার

কিছু UI স্টেটের ক্ষেত্রে, লাইব্রেরি ধরে নেয় যে সেগুলি বেশিরভাগ ক্ষেত্রে বাটন-লাইক কম্পোজ করার উপযুক্ত আইটেম দ্বারা ব্যবহার করা হবে।

রাজ্য মনে রেখো*স্টেট ধরন
PlayPauseButtonState rememberPlayPauseButtonState ২-টগল
PreviousButtonState rememberPreviousButtonState স্থির
NextButtonState rememberNextButtonState স্থির
RepeatButtonState rememberRepeatButtonState ৩-টগল
ShuffleButtonState rememberShuffleButtonState ২-টগল
PlaybackSpeedState rememberPlaybackSpeedState মেনু বা N-Toggle

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 সেট করা সম্পর্কে তথ্য দেখায়। আপনার কাস্টম UI-তে মেটাডেটা সংক্রান্ত তথ্য দেখানোর জন্য এগুলি উপযোগী।

ErrorState প্লেয়ারের বর্তমান সমস্যার অবস্থা সম্পর্কে তথ্য প্রদান করে, যা সমস্যার ওভারলে দেখানোর জন্য ব্যবহার করা যেতে পারে।

PresentationState-এ সেইসব তথ্য থাকে, যখন ভিডিও আউটপুট PlayerSurface দেখানো যেতে পারে বা প্লেসহোল্ডার UI এলিমেন্ট দিয়ে ঢেকে দেওয়া উচিত। ContentFrame কম্পোজ করার উপযুক্ত কোড অ্যাস্পেক্ট রেশিও ম্যানেজ করার সাথে সাথে এখনও রেডি হয়নি এমন কোনও সারফেসের উপর শাটার দেখানোর বিষয়টিও ম্যানেজ করে।

@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 ব্যবহার করে জানতে পারি যে Surface দেখানোর জন্য কখন সময় সঠিক নয়। এই ক্ষেত্রে, আপনি সারফেসের উপরে একটি অস্বচ্ছ শাটার পজিশন করতে পারেন, যা সারফেস রেডি হয়ে গেলে অদৃশ্য হয়ে যাবে। ContentFrame আপনাকে ট্রেলিং ল্যাম্বডা হিসেবে শাটার কাস্টমাইজ করতে দেয়, কিন্তু ডিফল্ট হিসেবে এটি একটি কালো @Composable Box যা প্যারেন্ট কন্টেনারের সাইজ পূরণ করে।

Flow কোথায় আছে?

Android ডেভেলপারদের মধ্যে অনেকেই, সবসময় পরিবর্তন হতে থাকা UI ডেটা সংগ্রহ করার জন্য Kotlin Flow অবজেক্ট ব্যবহার করার ব্যাপারে পরিচিত। যেমন, আপনি এমন Player.isPlaying ফ্লো খুঁজতে পারেন যা লাইফসাইকেল-সচেতন পদ্ধতিতে collect যায়। অথবা Player.eventsFlow-এর মতো কিছু যা আপনাকে Flow<Player.Events> প্রদান করবে যা আপনি যেভাবে চান filter করতে পারবেন।

তবে, Player UI স্টেটের জন্য ফ্লো ব্যবহার করার কিছু অসুবিধা আছে। ডেটা ট্রান্সফারের অ্যাসিঙ্ক্রোনাস নেচার হল অন্যতম প্রধান চিন্তার বিষয়। আমরা Player.Event এবং UI-এর দিকে সেটির ব্যবহারের মধ্যে যতটা সম্ভব কম লেটেন্সি রাখতে চাই, যাতে Player-এর সাথে সিঙ্ক না হওয়া UI এলিমেন্ট দেখানো এড়ানো যায়।

অন্যান্য পয়েন্টের মধ্যে রয়েছে:

  • সব Player.Events সহ ফ্লো একটিমাত্র দায়িত্বের নীতি মেনে চলে না, প্রতিটি কনজিউমারকে প্রাসঙ্গিক ইভেন্ট ফিল্টার করতে হবে।
  • প্রতিটি Player.Event-এর জন্য ফ্লো তৈরি করতে, আপনাকে প্রতিটি UI এলিমেন্টের জন্য সেগুলিকে (combine-এর সাথে) একত্রিত করতে হবে। Player.Event এবং UI এলিমেন্ট পরিবর্তনের মধ্যে অনেক-থেকে-অনেক ম্যাপিং আছে। combine ব্যবহার করতে হলে UI সম্ভাব্য বেআইনি স্টেটে চলে যেতে পারে।

কাস্টম UI স্টেট তৈরি করা

আগে থেকে থাকা UI স্টেট আপনার প্রয়োজন পূরণ না করলে, আপনি কাস্টম UI স্টেট যোগ করতে পারবেন। প্যাটার্ন কপি করতে, আগে থেকে থাকা স্টেটের সোর্স কোড চেক করে দেখুন। একটি সাধারণ UI স্টেট হোল্ডার ক্লাস নিম্নলিখিত কাজগুলি করে:

  1. Player অবজেক্ট নেয়।
  2. করুটিন ব্যবহার করে Player-এ সাবস্ক্রাইব করে। আরও বিবরণের জন্য Player.listen দেখুন।
  3. এর ইন্টার্নাল স্টেট আপডেট করে নির্দিষ্ট Player.Events-এর উত্তর দেয়।
  4. বিজনেস-লজিক কমান্ড গ্রহণ করে যা উপযুক্ত Player আপডেটে রূপান্তরিত হবে।
  5. UI ট্রি জুড়ে একাধিক জায়গায় তৈরি করা যেতে পারে এবং সবসময় প্লেয়ারের স্ট্যাটাসের একটি সামঞ্জস্যপূর্ণ ভিউ বজায় রাখবে।
  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 শুনতে দেয়। বিভিন্ন UI স্টেটের Media3 প্রয়োগ, এন্ড ডেভেলপারকে Player.Events সম্পর্কে জানার ব্যাপারে চিন্তা করতে দেয় না।