Fungjai
Published in

Fungjai

มาเล่นวิดีโอด้วย ExoPlayer กันเถอะ

เมื่อมีท่านนึงบอกว่าอยากอ่านบล็อก ExoPlayer ที่เป็นภาษาไทย และน้องบอกว่า แฟนอยากอ่านมาก แต่เป็นภาษาอังกฤษเลยกดปิดไป เอออออ แปลไทยให้ก็ได้ค่ะ

เราร่างบล็อกนี้ระหว่างทำสไลด์เพื่อนำไปพูดในงาน Android Bangkok 2018 หรืองาน Droidcon ในวันที่ 31 มีนาคมที่ผ่านมา ในหัวข้อ ExoPlayer ซึ่งได้นำมาใช้ในฟังใจแทนเจ้า MediaPlayer และนำไปใช้กับ product ใหม่ด้วยหล่ะ เรานำเรื่องนี้มาพูด เนื่องจากเป็นเรื่องเดียวที่เตรียมทันนั่นเอง เย้

รู้จักเพลงวันแรกเนอะ ชอบไหม เอ้ยยยยย รู้ไหมว่าใน YouTube บน Android ใช้ playback library ของอะไร

This is BNK48 lasted song and show about portrait and landscape

เราเลยขอแบ่ง topic ย่อยๆออกเป็น 3 ส่วน คือ แนะนำ, ใช้ยังไง และ เพิ่มเติม แปลเป็นภาษาไทยดูแปลกๆดีเนอะ 555

ถ้าอยากอ่านภาษาอังกฤษ ที่นี่เลยจ้า

พร้อมหรือยังค่ะ มาเริ่มกันเลยดีกว่า

แนะนำ

ก่อนที่จะทำแอปที่เล่น media ทั้งหลาย เราต้องรู้ก่อนว่าเจ้า ExoPlayer มันคืออะไร

อะไรคือ ExoPlayer?

ExoPlayer คือ open source media playback library สำหรับ Android
พัฒนาโดย Google ซึ่งถูกเขียนด้วยภาษา JAVA และข้อดีก็มีมากกว่าเจ้า MediaPlayer เช่น เงียบง่าย, มีความยืนหยุ่น, และ เสถียร

Exoplayer มี features มากมาย เช่น เล่นวีดิโอหรือเสียงได้, กด shuffle, เล่นเพลงวนซํ้า, subtitle, playlist, caching/downloading, เล่น ads ต่างๆก็ได้ด้วย, ทำ streaming, album art, offline, cast extension และอื่นๆ

มา Recap จาก Google I/O 2017

เราได้ทำการ recapped ข้อมูลสำคัญจากงาน Google I/O 17 ในวันที่ 17 พฤษภาคม 2017 ซึ่งมี speaker สองท่านคือ Oliver Woodman และ Andrew Lewis เป็น developer ที่ทำเจ้า ExoPlayer นั่นเอง

ref: https://www.youtube.com/watch?v=jAZn-J1I8Eg

Release Timeline

เรามาดูแต่ละ version ของ ExoPlayer กันดีกว่า ซึ่งตอน Google I/O ปีที่แล้วอยู่ในช่วง ExoPlayer 2.x พอดี และตอนนี้เป็น version 2.7.2 แล้ว (อัพก่อนวันงานวันนึง บ้าจริง)

  • r1.2.3 (25 มีนาคม 15) : เป็น version แรกที่ปล่อยให้ developer ใช้
  • r2.0.0 (14 กันยายน 16) : เป็น version แรกของ ExoPlayer 2.x ซึ่งเขาบอกว่าเลิกใช้ ExoPlayer 1.x ซะ
  • 2.6.0 (23 พฤศจิกายน 17) : ตอนนี้เลิกมี r อยู่ข้างหน้าเลข version หล่ะ
  • 2.7.0 (22 กุมภาพันธ์ 18) : มี feature ใหม่ๆเพียบ และ แก้บัคไปบางส่วน เช่น Player Interface, UI Component, Buffering, Cast Extension, Caching, และอื่นๆ ไปอ่านที่ release note เอานะ
  • 2.7.2 (29 มีนาคม 18) : เวอร์ชั่นล่าสุดจ้าา มีแก้บัคเล็กน้อย

สำหรับ release date ของแต่ละ version เราดูที่นี่

และ release note

อันนี้จะบอกโดยรวมทั้งในส่วน feature และไฟล์ต่างๆที่ support เช่น streaming format, audio & video, subtitle, metadata, extension.

ref: https://www.youtube.com/watch?v=jAZn-J1I8Eg

เปรียบเทียบ MediaPlayer vs ExoPlayer

ref: https://www.youtube.com/watch?v=jAZn-J1I8Eg
  • MediaPlayer รองรับทุก Android version เลย แต่ ExoPlayer รองรับ API level 16 ขึ้นไป

แต่ไม่ต้องกังวลไปเนอะ minimum android version ที่ใช้กันก็คือ API level 16 ซึ่งมีคนใช้ทั้งหมด 99.2% เรียกได้ว่าครอบคลุมยันจักรวาล

แคปเฌอ เอ้ยยย แคปมาจาก Android Studio
  • MediaPlayer ใช้แบบท่ายากไม่ได้เลย เช่น adaptive playback (สำหรับ streaming formation เช่น smooth streaming, DASH, และ HLS), media composition, caching, และอื่นๆอีกมากมาย
  • MediaPlayer เป็น black box ดังนั้นเราจะแก้อะไรไม่ได้เลย debug ก็ไม่ได้อีก แต่ ExoPlayer ตัวสไตล์สามารถปรับแต่งและขยายอะไรบางอย่างได้ด้วย

รูปนี้เปรียบเทียบให้เห็นเลยว่าระหว่าง MediaPlayer และ ExoPlayer อยู่ตรงไหนบ้าง สำหรับ MediaPlayer อยู่ใน Android OS และเหนือ Application เท่ากับว่า เราเรียกใช้ของสำเร็จรูป ส่วน ExoPlayer เราจะเรียกในแอปเรา และสามารถปรับอะไรได้ตามใจชอบเลย

ref: https://www.youtube.com/watch?v=jAZn-J1I8Eg

แล้วแอปอะไรใช้ ExoPlayer บ้างอ่ะ?

ตอนแรก Google เริ่มพัฒนา ExoPlayer และนำไปใช้ใน YouTube, Google Play Movie, Google Photos, Youtube gaming, Google Play Newsstand ก่อนที่จะปล่อยให้ developer ทั้งหลายได้ใช้กัน

ref: https://www.youtube.com/watch?v=jAZn-J1I8Eg

ใน Google Play Store นั้น มีมากกว่า 140,000 ที่ใช้ ExoPlayer ในแอปตัวเอง เพื่อเล่นไฟล์มีเดียต่างๆ เช่น Vevo, Twitter, BBC iPlayer, Netflix, Spotify, Facebook, Whatsapp, Twitch และรวมไปถึง Fungjai ด้วย

ref: https://www.youtube.com/watch?v=jAZn-J1I8Eg and add this text in my slide

แล้วมันใช้ยังไงอ่ะ?

ExoPlayer รองรับ Android 4.1 (API level 16) ขึ้นไป แต่บางอันอาจจะรองรับ version ที่สูงกว่านี้ ตามรูปเลยจ้า

Configuration

เราเพิ่ม dependency ของ ExoPlayer ในไฟล์ build.gradle ของ module ซึ่งเวอร์ชั่นล่าสุด คือ 2.7.2

implementation 'com.google.android.exoplayer:exoplayer:2.7.2'

จากนั้นมาเพิ่ม internet permission ใน manifest file เพื่อรับไฟล์มีเดียต่างๆจาก url ถ้าจะทำการ caching หรือเล่นไฟล์มีเดียบนเครื่องของ user ก็เพิ่ม read/write storage เอานะ

<uses-permission android:name=”android.permission.INTERNET”/> 
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Sample use

ใส่ลงไปใน layout file ได้เลยแบบนี้ ง่ายมากๆเลยใช่ไหมหล่ะตะเอง

<!-- activity_player.xml-->
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

เรามาเพิ่ม function สำหรับการอัญเชิญ player และสละมันทิ้งไปซะ

  • เพิ่ม initializePlayer() สำหรับการสร้าง Exoplayer ใหม่ใน player view เรา set ให้มันเล่นทันทีเมื่อ player พร้อมทำงาน และ seek ไปยัง window ล่าสุด ถ้า player ของเราถูกสร้างแล้ว ก็เตรียม media source จาก url.
  • เพิ่ม releasePlayer() เพื่อรับ playback position, current window value, play หรือ pause state, release player และให้เจ้า player เป็น null ซะ

แล้วเราจะใช้เจ้า ExoPlayer ใน Activity/Fragment ได้อย่างไร?

  • ใช้ initializePlayer() ที่onStart() และ onResume() สำหรับ init player
  • ใช้ releasePlayer() ที่ onPause() และ onStop() สำหรับ release player ก่อนที่หน้า Activity/Fragment จะถูกทำลายไป

Playback States

สำหรับ playback states จะมี 4 states ใน player

ref: http://google.github.io/ExoPlayer/doc/reference-v1/com/google/android/exoplayer/ExoPlayer.html
  1. Idle (Player.STATE_IDLE) : ไม่ได้เล่น media ใดๆ
  2. Buffering (Player.STATE.BUFFERING) : ขอโหลดข้อมูลเพิ่มหน่อยนะ
  3. Ready (Player.STATE_READY) : เล่น media บน playback ได้หล่ะ
  4. Ended (Player.STATE_END) : หยุดการเล่นที่ playback

ถ้าอยากจะจัดการอะไรพวกนี้ สามารถเข้าไปจัดการได้ที่ onPlayerStateChanged()ซึ่งเราจะต้อง implementation เจ้ Player.EventListenerก่อนนะ จึงจะใช้ได้

private String status;@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
switch (playbackState) {

case Player.STATE_BUFFERING:
status = PlaybackStatus.LOADING;
break;
case Player.STATE_ENDED:
status = PlaybackStatus.STOPPED;
break;
case Player.STATE_IDLE:
status = PlaybackStatus.IDLE;
break;
case Player.STATE_READY:
status = playWhenReady ? PlaybackStatus.PLAYING : PlaybackStatus.PAUSED;
break;
default:
status = PlaybackStatus.IDLE;
break;
}}

จากด้านบน ผลสุดท้ายเราจะได้แอปที่สามารถ streaming audio หรือ video ได้ และมี media playback ด้วย เห็นม่ะๆ

แต่ๆๆๆๆๆๆๆ แต่ลืมอะไรไปหรือเปล่า ในตอนทำ initialize และ release player นั้น เราลืมอะไรไปเอ่ยยย คิดสิคิด!

Supported formats

ในฟังก์ชั่น initializePlayer() เจ้า player ต้องมี MediaSource เพื่อเตรียม media จาก url และก็ supported formats ต่างๆกัน และใช้เจ้า MediaSource.Factory ต่างกันด้วย

  • แบบเบสิคๆก็ไฟล์ mp3 และ mp4 ไง :
return ExtractorMediaSource.Factory(
new DefaultHttpDataSourceFactory(userAgent)).createMediaSource(uri)
  • ถ้าเป็น playlist สำหรับ internet radio หรือ music streaming ก็ใช้เจ้า m3u8 นะ:
return HlsMediaSource.Factory(
DefaultHttpDataSourceFactory(userAgent)).createMediaSource(uri)
  • พวก streaming technologies ยากๆทั้งหลาย เช่น DASH, SmoothStreaming และ HLS
val dashChunkSourceFactory = DefaultDashChunkSource.Factory(DefaultHttpDataSourceFactory("ua", BANDWIDTH_METER))val manifestDataSourceFactory = DefaultHttpDataSourceFactory(userAgent)return DashMediaSource.Factory(
dashChunkSourceFactory, manifestDataSourceFactory).createMediaSource(uri)

Bonus : เราสามารถเล่นไฟล์ audio/video บน Exoplayer จากไฟล์ที่อยู่ในเครื่อง user เองได้ด้วยนะ จากการใช้ Uri.fromFile

val bandwidthMeter = DefaultBandwidthMeter()
val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(bandwidthMeter)
val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)
val defaultBandwidthMeter = DefaultBandwidthMeter()
val dataSourceFactory = DefaultDataSourceFactory(
context, Util.getUserAgent(context, "SongShakes"), defaultBandwidthMeter)
val videoSource = ExtractorMediaSource.Factory(dataSourceFactory).
createMediaSource(Uri.fromFile(File(filename)))

มาจัดการการหมุนจอ และไม่ให้เครื่องเรา lock screen ระหว่างที่เล่นวิดีโอ

เราดูวิดีโอไปเรื่อยๆ พอเราหมุนจอปุ๊ป อ้าว กลับไปเริ่มเล่นใหม่แหะ

แล้วเราจะแก้ปัญหานี้อย่างไรดีหล่ะ?

ตอนแรกคิดว่าจะใช้ Android Architecture Component แต่มันดูจัดการยุ่งยากซับซ้อน กับแค่อิหมุนจอเนี่ย เราก็ไม่ได้เก็บอะไรขนาดนั้นไง เอาจริงๆคือขี้เกียจ 555 (ถ้าว่างๆจะลองดูสักหน)

แต่เราขอใช้วิธีการแก้ปัญหาง่ายๆก่อนแล้วกัน ซึ่งมัน basic ที่สุดแล้ว และก็ไม่รู้ว่าคนอ่านจะ happy กับวิธีการแก้ปัญหาแบบนี้ไหมนะ การแก้ปัญหาในเบื้องต้น คือ การเพิ่ม config change สำหรับ player activity ที่ manifest file

<activity android:name=".PlayerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode">
</activity>

BONUS : ถ้าเราอยากเล่น video เต็มจอเฉพาะแนวนอนเท่านั้น และแนวตั้งยังเห็นเจ้า notification title ด้านบนอยู่ เราสามารถจัดการด้วย onConfigurationChanged ว่าจอนั้นหมุนไปแนวไหน แล้วก็ handle เองตามนี้

ผลที่ได้ คือ เล่นวิดีโอต่อเนื่องระหว่างหมุนจอ แล้วก็เต็มจอเฉพาะแนวนอนอย่างเดียวด้วย

และเราใส่ android:keepScreenOn=“true” ใน layout ที่เราวาง ExoPlayer activity_player.xml เพื่อไม่ให้จอดับระหว่างเล่นวิดีโอ

<LinearLayout     xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
...</LinearLayout>

มาแก้ปุ่มต่างๆของ playback บน player กันเถอะ

เราสามารถ custom user interface ของเจ้า player controller และ function ต่างๆ บน player ในแอปเรา

สำหรับการแก้ไขการทำงานต่างๆบน player : เราสามารถเพิ่ม behaviour attribute ของ playback ได้ ซึ่งทั้งหมดจะมีดังรูปนี้ และใครมันจะใช้มันทั้งหมดว่ะ จริงไหม

งั้นขอแสดงตัวอย่างหลังจากที่ใส่ฟังก์ชั่นบางอย่างเพิ่มไปด้วย

<com.google.android.exoplayer2.ui.SimpleExoPlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:use_controller="false"
app:show_timeout="10000"
app:repeat_toggle_modes="one"
app:fastforward_increment="10000"
app:rewind_increment="10000"/>
  • app:use_controller : default มันจะโชว์ playback controller ตลอดอยู่แล้ว ค่าเป็น “true” ถ้าจะซ่อนก็ใส่ “false” ซะ
  • app:show_timeout : เราจะให้ playback control ซ่อนหลังจากโชว์ให้ user ดูเป๋นเวลาเท่าไหร่ ในที่นี้ คือ 10 วินาที
  • app:repeat_toggle_modes : default มันจะไม่ repeat ค่าเป็น“none” ถ้าอยากให้เล่นซํ้าเพลงเดียว หรือวิดีโอเดียว เปลี่ยนค่าเป็น “one” และถ้าอยากให้วนซ้ทุกเพลงใน playlist ก็เปลี่ยนค่าเป็น “all”
  • app:fastforward_incerment : เวลาเรากดปุ่มนี้ มันจะเล่นวิดีโอข้างหน้า ในที่นี้ใช้ 10 วินาที คือกดแล้วมันจะไป 10 วินาทีให้หลัง
  • app:rewind_increment : เวลาเรากดปุ่มนี้ มันจะเล่นวิดีโอถอยหลัง ในที่นี้ใช้ 10 วินาที คือกดแล้วมันจะไป 10 วินาทีก่อนหน้า
  • app:resize_modes : เป็นโหมดปรับขนาดของ player โดยปกติค่าจะเป็น “fit” ตาม ratio ของ video หรืออยากให้มัน fit ไปเท่ากับ layout ก็ทำได้ โดยเปลี่ยนค่าเป็น “fill”

สำหรับการเปลี่ยนปุ่ม player controller :

ก่อนอื่นสร้างไฟล์นี้ก่อนเลยจ้า exo_playback_control_view.xml โดยเราจะทำการcustom playback style ด้วยวิธีการ override layout นั่นเองงง

เรามีสองตัวอย่างมาให้ดูกัน

ตัวอย่าง 1 :

  • playback ของเรามีปุ่ม play/pause, forward, และ rewind
  • เพิ่ม repeat button เข้าไปทีหลังจ้า

มาดูโดยรวมกันว่า ส่วนประกอบหลักๆของเจ้า exo_playback_control_view.xmlแบ่งเป็นสองส่วน คือ playback button และ timebar

เราสามารถแก้ปุ่มได้ทั้งหมด 7+1 ใน playback

  • 7 ปุ่มหลัก : previous, rewind, shuffle, play, pause, forward, next
  • 1 ปุ่มพิเศษ : repeat

สำหรับปุ่มหลัก ก็เพิ่มแสนจะง่ายเลย เพิ่มโดยการใส่เจ้า ImageButton แบบปกติเลย และสามารถแก้สี โดยใส่สีที่ต้องการที่ tint และรูปร่างปุ่มต่างๆเราใช้ style จาก ExoPlayer มาเลยจ้า

สำหรับปุ่ม repeat หา id มันเจอ แต่ไม่เจอสไตล์ของมันเลยแหะ งั้นใส่ style ของมันเป็น@Style/ExoMediaButton แบบนี้ไปก่อนจ้า

แล้วไปเพิ่ม app:repeat_toggle_modes=”one” ใน ExoPlayer บนไฟล์ player_activity.xml เพื่อให้มันสามารถกดปุ่ม repeat ได้

สรุปไฟล์ layout ของ exo_playback_control_view.xml สำหรับตัวอย่าง 1

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:background="#11000000"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="4dp"
android:orientation="horizontal">
<ImageButton android:id="@id/exo_rew"
android:tint="#FF5D29C1"
style="@style/ExoMediaButton.Rewind"/>
<ImageButton android:id="@id/exo_repeat_toggle"
android:tint="#FF5D29C1"
style="@style/ExoMediaButton"/>
<ImageButton android:id="@id/exo_play"
android:tint="#FF5D29C1"
style="@style/ExoMediaButton.Play"/>
<ImageButton android:id="@id/exo_pause"
android:tint="#FF5D29C1"
style="@style/ExoMediaButton.Pause"/>
<ImageButton android:id="@id/exo_ffwd"
android:tint="#FF5D29C1"
style="@style/ExoMediaButton.FastForward"/>
</LinearLayout> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView android:id="@id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FFFFFFFF"/>
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="16dp"/>
<TextView android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FFFFFFFF"/>
</LinearLayout></LinearLayout>

ตัวอย่าง 2 : แก้ปุ่ม play/pause และ timebar โดยได้รับคำบัญชาจาก UI designer

ขอแบ่งเป็น 2 ส่วนเพื่อความง่าย

(1) Playback : ก็แก้ ImageButton โดยการใส่ขนาด และ drawable ลงไป

<!-- Before -->
<ImageButton android:id="@id/exo_play"
android:tint="#FF5D29C1"
style="@style/ExoMediaButton.Play"/>
<!-- After -->
<ImageButton android:id="@id/exo_play"
android:layout_height="50dp"
android:layout_width="50dp"
android:background="@drawable/circle_purple_transparent"
android:src="@drawable/ic_play_player"/>

(2) TimeBar และ Duration:

สำหรับ duration นั้น แก้แบบ TextView ธรรมดาเลย

สำหรับ timebar สามารถเปลี่ยนขนาดและสี ที่ส่วนต่างๆใน timebar ได้เลย เช่น scrubber, played, unplay, และ buffer

Bonus : บางแอปเขาจะใช้ unplayed_color และ buffered_color สีเดียวกันนะ เราคิดว่าน่าจะขึ้นกับ UI/UX designer หรือ brand CI (ในที่นี้ คือ Color Index ไม่ใช่ Continuous Integration นะเออ)

สรุปไฟล์ layout ของ exo_playback_control_view.xml สำหรับตัวอย่างที่ 2

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:background="#11000000"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="4dp"
android:orientation="horizontal">
<ImageButton android:id="@id/exo_play"
android:layout_height="50dp"
android:layout_width="50dp"
android:background="@drawable/circle_purple_transparent"
android:src="@drawable/ic_play_player"/>
<ImageButton android:id="@id/exo_pause"
android:layout_height="50dp"
android:layout_width="50dp"
android:background="@drawable/circle_purple_transparent"
android:src="@drawable/ic_pause_player"/>
</LinearLayout> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView android:id="@id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FFFFFFFF"/>
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="16dp"
app:bar_height="1dp"
app:played_color="@color/colorAccent"
app:unplayed_color="#FFFFFFFF"
app:buffered_color="#FFFFFFFF"
app:scrubber_color="#FF5D29C1"/>
<TextView android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FFFFFFFF"/>
</LinearLayout></LinearLayout>

ปล. เราสามารถทำไฟล์ playback control viewใหม่ขึ้นมาได้เลยนะ และเพิ่มapp:controller_layout_id บน ExoPlayer ของเราได้เลย แบบนี้

<com.google.android.exoplayer2.ui.SimpleExoPlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:use_controller="true"
app:player_layout_id="@layout/exo_simple_player_view"
app:controller_layout_id="@layout/exo_playback_control_view"/>

ฺBonus : ถ้าเราอยากจะให้ play/pause โดยที่ไม่มี playback แบบนี้ เช่นมีแค่ปุ่มกดเล่น ระหว่างเล่นอยู่ปุ่มหายงี้ สามารถใส่โค้ดชุดนี้ลงไปได้เลย

การทำงาน player.playWhenReady = false เมื่อหยุดชั่วคราวและ player.playWhenReady = true เมื่อเล่นและเก็บ state ทั้งหมดไว้ใน playbackState

fun pausePlayer() {
player.playWhenReady = false
player.playbackState
}
fun startPlayer() {
player.playWhenReady = true
player.playbackState
}

ref:

Chunk List

ExoPlayer support chunk list สำหรับดาวน์โหลด stream media ในที่นี้เราพูดถึงการทำงานของ Chunk list นะ ไม่ใช่การใช้ Chunk ใน ExoPlayer นะ

ตัวอย่าง เราเล่นเพลงในเว็บ Fungjai (https://www.fungjai.com) สักเพลงนึง ระหว่างนั้นจะโหลด chunk list ขึ้นมา ดังด้านล่าง

มาอธิบายให้ชัดเจนกันดีกว่าหนาออเจ้า เช่น เราเล่น streaming music สักเพลงนึง เจ้า server จะแบ่งออกเป็นแต่ละส่วน เพื่อสามารถโหลด buffer มา ให้เล่นเพลงได้ smooth ซึ่งจะครอบคลุมเจ้า scrubber แต่ละก้อนจะเรียกว่า “Chunk”

อันนี้แอบกระซิบบอกนิดนึงตามประสาไม่เก่ง backend เหมือนจะใช้ wowza แบ่งโหลดนะ

สมมุติเราจิ้มไปตรงกลางๆเพลง server จะ check ดูว่า เห้ย มี buffer อยู่แถวนั้นไหม

อ้าว ไม่มี buffer ใช่ป่ะ โหลดมา

สำหรับขนาด นางจะแบ่งขนาดมาเท่าๆกัน เช่น ก้อนละ 10 วินาที ดังนั้นก้อนสุดท้ายมันไม่เท่ากับ 10 วินาทีเนอะ เพราะมันเป็นเศษเหลือนั่นเอง (ซึ่งเอาไปพูดเป็นอังกฤษยากมากเลยแหะ)

ใส่ Service ให้กับ Player กันดีกว่า

ฟังมาเยอะ อ่านมาเยอะ คงมีคำถามบางอย่างในใจกันใช่ม๊าา

  • อยากให้เพลงเล่นตอนที่ lock screen ได้อ่ะ เล่นระหว่างอยู่แอปอื่นก็ได้ด้วยนะ
  • พี่อยากได้ notification playback ด้วยนะ
  • อะไรประมาณนี้อ่ะ

ถ้าอยากได้ตามข้างบน แอปเราก็ต้องมี service สิจ๊ะ

ประเภทของ Service มี 3 ประเภท คือ Background Service (ไม่ได้ทำงานโดยตรงกับ user), Foreground Service (โชว์ serviceให้ user ดู แล้วมีพวก status bar icon มาด้วย), และ Bound Service (มีการคุยกันระหว่าง client และ server)

ดังนั้นแอปนี้ใช้ bound service เพราะว่ามีการคุยกันระหว่าง component และ service ตลอดเวลา และทำงานเป็น queue

ตัวอย่างแอปที่ใช้ bound service ในการเล่นเพลงแบบ background

การทำงานของ Service บน Exoplayer

  • PlayerNotificationManager เป็นคลาสที่จัดการเกี่ยวกับ notification playback ใน bound service
  • PlayerService จัดการการทำงานของ service บน player
  • PlayerManager จัดการเกี่ยวกับ service และถูกเรียกด้วยActivity/Fragment ที่ใช้ service

ตอนแรกสร้างไฟล์ PlayerService.java เพื่อสร้าง service ในแอปของเรา

public class PlayerService extends Service implements AudioManager.OnAudioFocusChangeListener, Player.EventListener {...}

และเพิ่มใน manifest file แบบนี้

<service android:name=".player.PlayerService"/>

ดังนั้นเราจะสร้าง bound service ตาม lifecycle ด้านขวาของรูปเลยเจ้าาา

ref : https://developer.android.com/guide/components/services.html
  • onCreate() : setup สิ่งจำเป็นที่ใช้ใน service เช่น media notification manager, media session, และ Exoplayer
  • onBind() : bind service โดย component จากการเรียก bindService() และส่ง data ที่ใช้โดย Intent.

ในเคสนี้จะคืนเจ้า service ของ class นี้

protected class PlayerBinder extends Binder {
PlayerService getService() {
return PlayerService.this;
}
}
private final IBinder playerBind = new PlayerBinder();@Nullable
@Override
public IBinder onBind(Intent intent) {
return playerBind;
}
  • onUnbind() : unbind service โดย component จากการเรียก unbindService()และส่ง boolean มาตัวนึงเพื่อ rebind service

ในเคสนี้ ถ้า playback status เราเป็น idle, player service จะหยุดจ้า

@Override
public boolean onUnbind(Intent intent) {
if (status.equals(PlaybackStatus.IDLE))
stopSelf();
return super.onUnbind(intent);
}
  • onDestroy() : reset value และคืน resource ไปสู่ system

ในเคสนี้ จะ release และลบ listener ใน player, ยกเลิก notify notification playback, และ release media session

@Override
public void onDestroy() {
pause();
exoPlayer.release();
exoPlayer.removeListener(this);
notificationManager.cancelNotify(); mediaSession.release(); super.onDestroy();
}

วิธีการใช้ service?

  • เราใช้ service.bind() ในที่ที่เราต้องการใช้ service และ service.unbind() สำหรับเลิกใช้ service
class PlayerFragment : Fragment() {
lateinit var playerManager: PlayerManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
playerManager = PlayerManager.with(context)
playerManager.bind()
...
}
...override fun onStop() {
super.onStop()
playerManager.unbind()
}
}

เรื่องนี้รู้สึกอธิบายยากจังเลย โค้ดก็เยอะมาก อ่านแล้วคงจะงงกันใช่ไหมหล่ะ

เราจะนำคุณไปสู่โปรเจกของเรา เพื่อจะมาดูโค้ดกัน

และดูได้ที่นี้เลยจ้า โอ้ยยยย เหนื่อยจังเลย

เพิ่มเติม : สำหรับข้อมูลเพิ่มเติมในเรื่องนี้

เราเก็บรวบรวมข้อมูล ExoPlayer ที่เราใช้ เยอะแยะมากมาย ดังนี้

Official ExoPlayer Resource

  • ExoPlayer github

และ guideline

  • developer ของ ExoPlayer เขาเขียน blog บน medium ด้วยนะเออ ในนั้นบอกถึง feature ใหม่ของบาง version วิธีการใช้ feature ต่างๆ tutorial ต่างๆ
  • อ่านแล้วอยากลองเขียนใช่ไหมหล่ะ เริ่มจากเจ้า codelab ก่อนเลย

libraries ที่น่าสนใจ

  • อันนี้ custom ExoPlayer ซึ่งใช้ง่ายยังกะ VideoView

สำหรับ trim video library ใช้ตัวนี้

  • อยากเพิ่ม filter video ใช้ตัวนี้เลยจ้าาา

และสามารถเซฟวิดีโอ with filter ได้ด้วย แต่ใช้ตัวนี้นะ

แต่ถ้าเราเซฟวิดีโอที่มี filter และลายนํ้า เราต้องเซฟสองรอบอ่ะ เพราะมันมองลายนํ้าเป็น filter ตัวนึง แถม process นานอีก ฮือออ

  • สำหรับ Caching ใน ExoPlayer บรรดา developer มีปัญหากันหลายๆ issue เกิดจากความไม่เคลียร์ในการใช้งาน จนมีคนนึงแนะนำว่าใช้ library ตัวนี้สิ แต่ก็มีปัญหา คือ cahce video ทั้งไฟล์เลยจ้า กิน storage เราไปอีกกก
reference from https://github.com/google/ExoPlayer/issues/420#issuecomment-204251112

ดังนั้น developer ของ Exoplayer เขียนบล็อก tutorial สำหรับ caching บน medium ซะเลย บ่นกันเยอะใช่ไหม ห๊ะ

  • สิ่งที่เราสนใจอีกเรื่อง คือ cast extension, สามารถ cast จากมือถือของเรา ไป smart device อื่นๆได้ เช่น ทีวี

สุดท้าย #พื้นที่โฆษณา

Fungjai เป็นแอปฟังเพลงแบบ streaming ทำโดยคนไทย 100% เลย ศิลปินที่มาลงเพลงนั้นนอกจากคนไทยยังมีชาติอื่นๆอีกด้วย เช่น ญี่ปุ่น ไต้หวัน อินโดนีเซีย และมีหลายๆ feature ที่น่าสนใจ ดังนี้

  • Discover : Top 20 charts, Recommend, New Album, และ New Artist
  • Browse : สามารถเลือกเพลงตามอารมณ์หรือแนวเพลงได้
  • Playlist : จัดโดย Fungjai staff นั่นแหละ แบ่งเป็น 3 type ยังกะ photobook BNK48 เลย มี Mood & Genre Playlist, Theme Playlist (heartbroken), และ Feature Playlist (Recommend song, re-live, DJ).
  • My Music : บรรดาเพลงที่ชอบ เพลย์ลิสต์ที่ชอบ อัมบั้มที่ชอบ และเพลงที่ฟังน่าสุด มีฟังออฟไลน์ด้วยนะ

แอปฟังใจใช้ ExoPlayer กับตัว player นั่นแหละ และเพิ่ม service ตามที่เรากล่าวไปเบื้องต้น

ปล. กด like หรือ follow เพจของเราได้น๊าาาา เราจะลงบทความเราเองด้วย และบทความที่น่าสนใจจากท่านอื่นๆที่เราอยากแบ่งปัน

จริงๆเราเขียนบล็อกอยู่ในนี้ :D

--

--

Music community startups in Thailand

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Minseo Chayabanjonglerd

Android Developer | Content Creator AKA. MikkiPastel | Moderator of Stocker DAO