Kiến Trúc Quản Lý Phiên Bản Đa Phương Tiện Jellyfin
Với tư cách là một ứng dụng khách chính thức cho máy chủ đa phương tiện nguồn mở Jellyfin, phiên bản Android tập trung mạnh vào việc cung cấp trải nghiệm người dùng liền mạch khi tương tác với nội dung streaming. Hai khía cạnh cốt lõi làm nên chất lượng này là hệ thống thông báo (notification) hiển thị trạng thái và khả năng duy trì phát nội dung ngay cả khi ứng dụng chuyển sang nền. Việc nắm vững cơ chế hoạt động của các thành phần này giúp tối ưu hóa quy trình phát triển các ứng dụng phát media tương tự.
Hệ Thống Điều Khiển Thông Báo
Bộ phận chịu trách nhiệm hiển thị trạng thái chơi nhạc/phim trên thanh trạng thái được xây dựng dựa trên API Notification của Android. Mục tiêu chính là giữ cho người dùng có thể kiểm soát tiến trình mà không cần mở lại giao diện ứng dụng.
Cấu Trúc Class Hỗ Trợ
Tập tin PlayerNotificationHelper.kt đóng vai trò trung tâm trong việc quản lý đối tượng thông báo. Class này encapsulate logic tạo ra UI thông báo, định nghĩa kênh thông báo (notification channel) và xử lý các sự kiện từ nút bấm.
Kênh Và Mức Độ Ưu Tiên
Để đảm bảo tính ổn định và tuân thủ các chính sách của hệ điều hành, ứng dụng thiết lập một kênh riêng biệt dành cho việc stream media. Hàm khởi tạo kênh này xác định mức độ ưu tiên và quyền hiển thị, giúp tránh bị chặn bởi các bộ lọc thông báo mặc định.
Xử Lý Sự Kiện Người Dùng
Mỗi thao tác như chuyển bài, tạm dừng hoặc tiếp tục đều được ánh xạ vào một Intent cụ thể thông qua Enum PlayerNotificationAction. Khi người dùng kích hoạt nút điều khiển trên màn hình khóa hoặc thanh thông báo, hệ thống sẽ nhận diện action và thực thi lệnh tương ứng lên bộ nhớ đệm (buffer) của player.
Chế Độ Phát Nền (Background Playback)
Năng lực duy trì luồng dữ liệu khi ứng dụng ẩn đi đòi hỏi sự kết hợp giữa service foreground và session quản lý media. Đây là yếu tố then chốt để ngăn chặn hệ thống Android hủy process nhằm tiết kiệm tài nguyên.
Vai Trò Của MediaSession
Trong PlayerViewModel.kt, instance của MediaSession được khởi tạo để kết nối logic phát lại với khung hệ thống. Nó cung cấp token cho thông báo và lắng nghe các yêu cầu vận chuyển (transport controls) từ phần cứng hoặc phần mềm bên ngoài.
Quản Lý Vòng Đời
Sử dụng PlayerLifecycleObserver giúp theo dõi trạng thái ứng dụng. Khi chuyển sang chế độ nền, thay vì ngắt kết nối, luồng phát tiếp tục chạy. Trạng thái hiện tại được lưu trữ để khôi phục ngay lập tức khi người dùng quay lại giao diện chính.
Triển Khai Mã Nguồn Tham Khảo
Dưới đây là đoạn mã đã được tối ưu hóa cấu trúc, minh họa cách xây dựng đối tượng thông báo và thiết lập session.
// Cập nhật logic hiển thị thông báo
private fun dispatchCurrentState(context: Context, currentTrack: TrackModel) {
val notifMgr = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Đảm bảo kênh thông báo tồn tại trước khi gửi
createSpecificMediaChannel(notifMgr)
// Cấu hình style cho trình phát
val mediaStyleBuilder = NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0, 1, 2) // Hiển thị 3 nút đầu ở dạng gọn
.setMediaSession(sessionTokenProvider.get())
val notificationBuilder = NotificationCompat.Builder(context, MEDIA_CHANNEL_ID)
.setContentTitle(currentTrack.name)
.setContentText(currentTrack.artist)
.setSmallIcon(R.drawable.ic_jellyfin_logo)
.setLargeIcon(loadAlbumArtUrl(currentTrack.artId))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setStyle(mediaStyleBuilder)
// Thêm các hành động tùy chỉnh
notificationBuilder.addAction(buildControlIntent(ActionType.SKIP_PREV, "Prev"))
notificationBuilder.addAction(buildControlIntent(ActionType.TOGGLE_PAUSE, "Play/Pause"))
notificationBuilder.addAction(buildControlIntent(ActionType.SKIP_NEXT, "Next"))
notifMgr.notify(NOTIFICATION_UNIQUE_ID, notificationBuilder.build())
}
// Khởi tạo phiên kết nối Media
fun initializeMediaSession(appCtx: Context): MediaSession {
return MediaSession(appCtx, "Jellyfin-Media-Tag").apply {
setFlags(
MediaSession.FLAG_HANDLES_MEDIA_BUTTONS or
MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS
)
callback = object : MediaSession.Callback() {
override fun onPlay() = viewModel.handlePlay()
override fun onPause() = viewModel.handlePause()
override fun onSkipToPrevious() = viewModel.seekPrevious()
override fun onSkipToNext() = viewModel.seekNext()
}
isActive = true
}
}