Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Android

วิธีเล่นวิดีโอด้วย TextureView ใน Android

ภาพรวม

บทความนี้สาธิตวิธีเล่นวิดีโอด้วย 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

  1. คลิกขวาที่โฟลเดอร์ app/src/main → New → Folder → Assets Folder
  2. วางไฟล์วิดีโอ (เช่น 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

คุณสมบัติTextureViewVideoViewSurfaceView
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