কোর কম্পোজ

media3-ui-compose লাইব্রেরিটি জেটপ্যাক কম্পোজে একটি মিডিয়া ইউআই তৈরির জন্য প্রয়োজনীয় মৌলিক উপাদান সরবরাহ করে। এটি এমন ডেভেলপারদের জন্য ডিজাইন করা হয়েছে, যাদের media3-ui-compose-material3 লাইব্রেরির চেয়ে বেশি কাস্টমাইজেশনের প্রয়োজন। এই পৃষ্ঠায় মূল উপাদান এবং স্টেট হোল্ডারগুলো ব্যবহার করে কীভাবে একটি কাস্টম মিডিয়া প্লেয়ার ইউআই তৈরি করা যায়, তা ব্যাখ্যা করা হয়েছে।

ম্যাটেরিয়াল৩ এবং কাস্টম কম্পোজ উপাদান মিশ্রণ

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 মধ্যে থাকা Raw surface, যা SurfaceView এবং TextureView আবৃত করে।

Player রচনাযোগ্য অংশ কাস্টমাইজ করুন

media3-ui-compose-material3 লাইব্রেরি ব্যবহার করার সময়, Player কম্পোজেবলটি একটি সম্পূর্ণ মিডিয়া প্লেব্যাক অভিজ্ঞতা প্রদান করে। আপনি এর কন্টেন্ট স্লটগুলিতে আপনার নিজস্ব কম্পোজেবল যোগ করে এর লেআউট কাস্টমাইজ করতে পারেন, অথবা UI-এর নির্দিষ্ট অংশ, যেমন TopControls , CenterControls , BottomControls , এবং ErrorOverlay , কাস্টমাইজ করার জন্য PlayerDefaults অবজেক্টটি ব্যবহার করতে পারেন।

উদাহরণস্বরূপ, আপনি 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 স্টেট হোল্ডার এবং কম্পোজেবল-এর মধ্যেকার নমনীয়তা আপনি কীভাবে ব্যবহার করতে পারেন, তা আরও ভালোভাবে বুঝতে হলে কম্পোজ কীভাবে স্টেট পরিচালনা করে সে সম্পর্কে পড়ুন।

বাটন অবস্থা ধারক

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

রাজ্য মনে রাখবেন*রাজ্য প্রকার
PlayPauseButtonState rememberPlayPauseButtonState ২-টগল
PreviousButtonState rememberPreviousButtonState ধ্রুবক
NextButtonState rememberNextButtonState ধ্রুবক
RepeatButtonState rememberRepeatButtonState ৩-টগল
ShuffleButtonState rememberShuffleButtonState ২-টগল
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 গুলোর তথ্য প্রকাশ করে। আপনার কাস্টম 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 (আরও প্রকারের জন্য কন্টেন্ট স্কেল দেখুন) এবং সারফেসটি দেখানোর জন্য সময় কখন সঠিক নয় তা জানার জন্য presentationState.coverSurface উভয়ই ব্যবহার করতে পারি। এক্ষেত্রে, আপনি সারফেসের উপরে একটি অস্বচ্ছ শাটার স্থাপন করতে পারেন, যা সারফেসটি প্রস্তুত হলে অদৃশ্য হয়ে যাবে। ContentFrame আপনাকে শাটারটিকে একটি ট্রেইলিং ল্যাম্বডা হিসেবে কাস্টমাইজ করার সুযোগ দেয়, কিন্তু ডিফল্টরূপে এটি প্যারেন্ট কন্টেইনারের আকার পূরণকারী একটি কালো @Composable Box হবে।

প্রবাহগুলো কোথায়?

অনেক অ্যান্ড্রয়েড ডেভেলপার ক্রমাগত পরিবর্তনশীল UI ডেটা সংগ্রহ করার জন্য কোটলিন 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. কম্পোজ 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 সম্পর্কে শেখার ঝামেলা থেকে মুক্তি দেয়।