ภาพรวม
บทความนี้สาธิตวิธีเล่นวิดีโอด้วย TextureView ใน Android ซึ่งมีข้อได้เปรียบเหนือ VideoView คือ: รองรับการ Transform (Rotate, Scale, Alpha), ใช้ได้ใน View Hierarchy ปกติ, และรองรับ Hardware Acceleration ดีกว่า เหมาะสำหรับ Custom Video Player, Picture-in-Picture, หรือ AR Applications
ขั้นตอนที่ 1: สร้างโปรเจกต์ใหม่
สร้างโปรเจกต์ Android ใหม่ใน Android Studio
ขั้นตอนที่ 2: เพิ่มวิดีโอใน Assets
- คลิกขวาที่โฟลเดอร์
app/src/main→ New → Folder → Assets Folder - วางไฟล์วิดีโอ (เช่น
videoplayback.mp4) ลงในapp/src/main/assets/
ขั้นตอนที่ 3: ออกแบบ Layout (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
tools:context=".MainActivity">
<TextureView
android:id="@+id/textureView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
ขั้นตอนที่ 4: เขียนโค้ด MainActivity (src/MainActivity.java) - เวอร์ชันปรับปรุง
import androidx.appcompat.app.AppCompatActivity;
import android.content.res.AssetFileDescriptor;
import android.graphics.SurfaceTexture;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.Bundle;
import android.view.Surface;
import android.view.TextureView;
import android.widget.Toast;
import java.io.IOException;
public class MainActivity extends AppCompatActivity
implements TextureView.SurfaceTextureListener, MediaPlayer.OnVideoSizeChangedListener {
private TextureView textureView;
private MediaPlayer mediaPlayer;
private AssetFileDescriptor fileDescriptor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textureView = findViewById(R.id.textureView);
textureView.setSurfaceTextureListener(this);
mediaPlayer = new MediaPlayer();
mediaPlayer.setOnVideoSizeChangedListener(this);
mediaPlayer.setOnErrorListener((mp, what, extra) -> {
Toast.makeText(this, "Video Error: " + what, Toast.LENGTH_SHORT).show();
return true;
});
mediaPlayer.setOnCompletionListener(mp -> {
// Loop วิดีโอหากต้องการ
// mp.seekTo(0);
// mp.start();
});
try {
fileDescriptor = getAssets().openFd("videoplayback.mp4");
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "ไม่พบไฟล์วิดีโอใน Assets", Toast.LENGTH_LONG).show();
}
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
Surface surface = new Surface(surfaceTexture);
try {
mediaPlayer.setSurface(surface);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// API 24+ ใช้ setDataSource(AssetFileDescriptor) โดยตรง
mediaPlayer.setDataSource(fileDescriptor);
} else {
// API เก่า ใช้ getDeclaredLength etc.
mediaPlayer.setDataSource(fileDescriptor.getFileDescriptor(),
fileDescriptor.getStartOffset(), fileDescriptor.getDeclaredLength());
}
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(mp -> {
mp.start();
// ปรับ Aspect Ratio ให้พอดี TextureView
adjustAspectRatio(mp.getVideoWidth(), mp.getVideoHeight());
});
} catch (IOException e) {
e.printStackTrace();
}
}
private void adjustAspectRatio(int videoWidth, int videoHeight) {
if (videoWidth > 0 && videoHeight > 0) {
float videoAspect = (float) videoWidth / videoHeight;
float viewAspect = (float) textureView.getWidth() / textureView.getHeight();
// Scale TextureView ให้ Aspect Ratio ตรงกับวิดีโอ
if (videoAspect > viewAspect) {
textureView.setScaleX(videoAspect / viewAspect);
} else {
textureView.setScaleY(viewAspect / videoAspect);
}
}
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
// ปรับ Aspect Ratio เมื่อขนาด View เปลี่ยน
if (mediaPlayer != null) {
adjustAspectRatio(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight());
}
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
// คืนค่า true เพื่อปล่อย Surface
releaseMediaPlayer();
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
// เรียกทุก Frame - ใช้สำหรับ Filter/Effect real-time
}
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
adjustAspectRatio(width, height);
}
private void releaseMediaPlayer() {
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
releaseMediaPlayer();
if (fileDescriptor != null) {
try {
fileDescriptor.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
protected void onPause() {
super.onPause();
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.pause();
}
}
@Override
protected void onResume() {
super.onResume();
if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
mediaPlayer.start();
}
}
}
ขั้นตอนที่ 5: AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="app.com.sample">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
ข้อได้เปรียบของ TextureView vs VideoView/SurfaceView
| คุณสมบัติ | TextureView | VideoView | SurfaceView |
|---|---|---|---|
| Transform (Rotate/Scale/Alpha) | ✅ รองรับเต็มรูปแบบ | ❌ ไม่รองรับ | ❌ ไม่รองรับ |
| Z-Ordering | ✅ ปกติใน View Hierarchy | ✅ ปกติ | ❌ อยู่ด้านหลัง Window |
| Screenshot/Screen Record | ✅ จับได้ | ✅ จับได้ | ❌ จับไม่ได้ (Secure) |
| Hardware Acceleration | ✅ ต้องเปิด (Default API 14+) | ✅ | ✅ |
| Performance | ดีน้อยกว่า SurfaceView เล็กน้อย | ดี | ดีที่สุด |
เทคนิคขั้นสูง
- Video Filters/Effects: ใช้
onSurfaceTextureUpdated+ OpenGL ES เพื่อใส่ Filter แบบ Real-time - Picture-in-Picture: TextureView ย่อได้ง่ายด้วย setScaleX/Y
- Streaming (HLS/DASH): ใช้ ExoPlayer แทน MediaPlayer (`mediaPlayer.setSurface(surface)` เหมือนกัน)
- DRM Content: ใช้ MediaDrm + ExoPlayer
การแก้ไขปัญหา (Troubleshooting)
- ดำจอ/ไม่เล่น: ตรวจสอบ Asset path, File format (MP4/H.264), Hardware Acceleration
- เสียงมี/ภาพไม่มี: ตรวจสอบ SurfaceTextureListener, onSurfaceTextureAvailable
- Memory Leak: ต้อง release MediaPlayer ใน onSurfaceTextureDestroyed และ onDestroy
- Orientation Change: TextureView จะ recreate Surface ให้ handle ใน onSurfaceTextureDestroyed/Available