ฉันจะบันทึกวิดีโอในแอพ Android ของฉันได้อย่างไร


137

ฉันจะบันทึกวิดีโอบน Android ได้อย่างไร


เราสามารถบันทึกเสียงในแทร็กวิดีโอที่มีอยู่ได้หรือไม่?
Ahmad Arslan

วิธีการตรวจสอบคำขอบคุณที่เป็นประโยชน์อีกข้อหนึ่ง
Adnan Abdollah Zaki

คำตอบ:


165

นี่คือตัวอย่างการบันทึกวิดีโออย่างง่าย ๆ โดยใช้ MediaRecorder:

public class VideoCapture extends Activity implements OnClickListener, SurfaceHolder.Callback {
    MediaRecorder recorder;
    SurfaceHolder holder;
    boolean recording = false;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

        recorder = new MediaRecorder();
        initRecorder();
        setContentView(R.layout.main);

        SurfaceView cameraView = (SurfaceView) findViewById(R.id.CameraView);
        holder = cameraView.getHolder();
        holder.addCallback(this);
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

        cameraView.setClickable(true);
        cameraView.setOnClickListener(this);
    }

    private void initRecorder() {
        recorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
        recorder.setVideoSource(MediaRecorder.VideoSource.DEFAULT);

        CamcorderProfile cpHigh = CamcorderProfile
                .get(CamcorderProfile.QUALITY_HIGH);
        recorder.setProfile(cpHigh);
        recorder.setOutputFile("/sdcard/videocapture_example.mp4");
        recorder.setMaxDuration(50000); // 50 seconds
        recorder.setMaxFileSize(5000000); // Approximately 5 megabytes
    }

    private void prepareRecorder() {
        recorder.setPreviewDisplay(holder.getSurface());

        try {
            recorder.prepare();
        } catch (IllegalStateException e) {
            e.printStackTrace();
            finish();
        } catch (IOException e) {
            e.printStackTrace();
            finish();
        }
    }

    public void onClick(View v) {
        if (recording) {
            recorder.stop();
            recording = false;

            // Let's initRecorder so we can record again
            initRecorder();
            prepareRecorder();
        } else {
            recording = true;
            recorder.start();
        }
    }

    public void surfaceCreated(SurfaceHolder holder) {
        prepareRecorder();
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        if (recording) {
            recorder.stop();
            recording = false;
        }
        recorder.release();
        finish();
    }
}

มาจากหนังสือของฉัน: Pro Android Media: การพัฒนาแอพกราฟิกเพลงวิดีโอและสื่อสมบูรณ์สำหรับสมาร์ทโฟนและแท็บเล็ต

นอกจากนี้อย่าลืมรวมสิทธิ์เหล่านี้ไว้ในรายการ:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

29
นี่เป็นตัวอย่างที่ดี ฉันมีปัญหาในการทำให้การทำงานในตอนแรก ข้อผิดพลาดของฉันเกี่ยวข้องกับขนาดเอาต์พุตไฟล์ 0 ไบต์ ฉันเริ่มแก้จุดบกพร่องและผู้ร้ายกลายเป็น record.prepare () ฉันไม่ได้ตั้งค่า outputfile ใหม่สำหรับเครื่องบันทึกดังนั้นจึงเป็นการเขียนทับวิดีโอที่มีอยู่ทุกครั้งที่ฉันแตะหน้าจอเพื่อหยุดการบันทึกวิดีโอ หวังว่านี่จะช่วยให้คนอื่นอยู่ข้างนอกนั่น :-) โดยวิธีที่ควรค่าแก่การกล่าวถึงว่าคุณต้องเพิ่ม RECORD_AUDIO, CAMERA และ WRITE_EXTERNAL_STORAG ​​E ที่สิทธิ์ของรายการของคุณเพื่อให้โค้ดตัวอย่างนี้ทำงาน
Lasse Samson

11
โปรดโพสต์ไฟล์ xml ด้วย
Nirav Ranpara

16
นี่คือตัวอย่างด้านบนพร้อมตัวอย่างกล้อง: github.com/vanevery/Custom-Video-Capture-with-Preview
vanevery

3
คำเตือน! หากคุณจะใช้ตัวอย่างนี้อย่าลืมลบเมธอด finish () จากการทำลายพื้นผิว! เป็นการปิดโอกาสให้คุณกลับสู่กิจกรรมนี้จากกิจกรรมอื่น ๆ ฉันเสียเวลาไปมากกว่า 2 ชั่วโมง)
Divers

3
@vanevery ปัญหาใหญ่ของรหัสนี้คือ .. มันสร้างไฟล์สองไฟล์หนึ่งคือการบันทึกที่ดีอีกไฟล์หนึ่งคือการสร้างเมื่อเราเตรียมเครื่องบันทึก .. มีวิธีใดที่จะลบไฟล์นั้นถ้าเราไม่บันทึกมัน ..
Swap-IOS -Android

46

นี่เป็นอีกตัวอย่างที่ใช้งานได้

public class EnregistrementVideoStackActivity extends Activity implements SurfaceHolder.Callback {
    private SurfaceHolder surfaceHolder;
    private SurfaceView surfaceView;
    public MediaRecorder mrec = new MediaRecorder();
    private Button startRecording = null;

    File video;
    private Camera mCamera;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.camera_surface);
        Log.i(null , "Video starting");
        startRecording = (Button)findViewById(R.id.buttonstart);
        mCamera = Camera.open();
        surfaceView = (SurfaceView) findViewById(R.id.surface_camera);
        surfaceHolder = surfaceView.getHolder();
        surfaceHolder.addCallback(this);
        surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        menu.add(0, 0, 0, "StartRecording");
        menu.add(0, 1, 0, "StopRecording");
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item)
    {
        switch (item.getItemId())
        {
        case 0:
            try {
                startRecording();
            } catch (Exception e) {
                String message = e.getMessage();
                Log.i(null, "Problem Start"+message);
                mrec.release();
            }
            break;

        case 1: //GoToAllNotes
            mrec.stop();
            mrec.release();
            mrec = null;
            break;

        default:
            break;
        }
        return super.onOptionsItemSelected(item);
    }

    protected void startRecording() throws IOException 
    {
        mrec = new MediaRecorder();  // Works well
        mCamera.unlock();

        mrec.setCamera(mCamera);

        mrec.setPreviewDisplay(surfaceHolder.getSurface());
        mrec.setVideoSource(MediaRecorder.VideoSource.CAMERA);
        mrec.setAudioSource(MediaRecorder.AudioSource.MIC); 

        mrec.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
        mrec.setPreviewDisplay(surfaceHolder.getSurface());
        mrec.setOutputFile("/sdcard/zzzz.3gp"); 

        mrec.prepare();
        mrec.start();
    }

    protected void stopRecording() {
        mrec.stop();
        mrec.release();
        mCamera.release();
    }

    private void releaseMediaRecorder(){
        if (mrec != null) {
            mrec.reset();   // clear recorder configuration
            mrec.release(); // release the recorder object
            mrec = null;
            mCamera.lock();           // lock camera for later use
        }
    }

    private void releaseCamera(){
        if (mCamera != null){
            mCamera.release();        // release the camera for other applications
            mCamera = null;
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        if (mCamera != null){
            Parameters params = mCamera.getParameters();
            mCamera.setParameters(params);
        }
        else {
            Toast.makeText(getApplicationContext(), "Camera not available!", Toast.LENGTH_LONG).show();
            finish();
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mCamera.stopPreview();
        mCamera.release();
    }
}

camera_surface.xml

<?xml version="1.0" encoding="UTF-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<SurfaceView
    android:id="@+id/surface_camera"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1" />

<Button
    android:id="@+id/buttonstart"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/record_start" />

</RelativeLayout>

และแน่นอนรวมถึงการอนุญาตเหล่านี้ในรายการ:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

สวัสดีตรวจสอบสิ่งนี้: stackoverflow.com/questions/10594553/…
Ponmalar

ขอบคุณตัวอย่างของ vanevery คือการจัดเก็บไฟล์ขนาด 0 ไบต์ซึ่งไม่เป็นประโยชน์
user609239

โดยการคลิกที่เมนูฮาร์ดแวร์การเริ่มและหยุดการบันทึกจะปรากฏขึ้น แต่ในแท็บเล็ต Android ไม่มีเมนูฮาร์ดแวร์ ในสถานการณ์สมมตินั้นฉันจะเริ่มและหยุดการจับภาพวิดีโอได้อย่างไร
Karthick

บางคนอาจได้รับตัวอย่างเพื่อรวบรวม แต่มันก็ใช้งานได้ดีสำหรับฉัน ขอบคุณ!
Gopherkhan

@Milos คุณช่วยบอกวิธีการฝังภาพเครื่องหมายในวิดีโอพื้นผิวได้หรือไม่?
Ahmad Arslan

5

คุณบันทึกเสียงและวิดีโอโดยใช้คลาส MediaRecorder เดียวกัน มันค่อนข้างง่าย นี่คือตัวอย่าง


1
มีข้อบกพร่องเล็ก ๆ น้อย ๆ ในการใช้งานวิดีโอใน MediaRecorder ที่ทำให้เกิดข้อผิดพลาดในการแบ่งกลุ่มด้วยเหตุผลที่ไม่สามารถคาดการณ์ได้ ฉันสงสัยว่านี่เป็นสาเหตุที่ @Vishnuparsad โพสต์คำถามนี้ตั้งแต่แรก
bobpoekert

จากนั้นเขาก็ควรจะมีการพูดถึงเรื่องนี้ :)
Drakosha

การบันทึกวิดีโอต้องใช้เวลาอีกเล็กน้อยในการจัดการกับหน้าตัวอย่าง
vanevery

5

การสาธิตนี้จะเป็นประโยชน์สำหรับคุณ ....

video.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >

<ToggleButton
    android:id="@+id/toggleRecordingButton"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true" />

<SurfaceView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/surface_camera"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_centerInParent="true"
    android:layout_weight="1" >
</SurfaceView>

กิจกรรมหลักของคุณ: Video.java

 public class Video extends Activity implements OnClickListener,
    SurfaceHolder.Callback {

private static final String TAG = "CAMERA_TUTORIAL";

private SurfaceView mSurfaceView;
private SurfaceHolder mHolder;
private Camera mCamera;
private boolean previewRunning;
private MediaRecorder mMediaRecorder;
private final int maxDurationInMs = 20000;
private final long maxFileSizeInBytes = 500000;
private final int videoFramesPerSecond = 20;
Button btn_record;
boolean mInitSuccesful = false;
File file;
ToggleButton mToggleButton;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.video);

    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

    mSurfaceView = (SurfaceView) findViewById(R.id.surface_camera);
    mHolder = mSurfaceView.getHolder();
    mHolder.addCallback(this);
    mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

    mToggleButton = (ToggleButton) findViewById(R.id.toggleRecordingButton);
    mToggleButton.setOnClickListener(new OnClickListener() {
        @Override
        // toggle video recording
        public void onClick(View v) {
            if (((ToggleButton) v).isChecked())
                mMediaRecorder.start();
            else {
                mMediaRecorder.stop();
                mMediaRecorder.reset();
                try {
                    initRecorder(mHolder.getSurface());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    });
}

private void initRecorder(Surface surface) throws IOException {
    // It is very important to unlock the camera before doing setCamera
    // or it will results in a black preview
    if (mCamera == null) 
    {
        mCamera = Camera.open();
        mCamera.unlock();
    }

    if (mMediaRecorder == null)
        mMediaRecorder = new MediaRecorder();

    mMediaRecorder.setPreviewDisplay(surface);
    mMediaRecorder.setCamera(mCamera);

    mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

    mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);

    mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);

    mMediaRecorder.setOutputFile(this.initFile().getAbsolutePath());

    // No limit. Don't forget to check the space on disk.
    mMediaRecorder.setMaxDuration(50000);
    mMediaRecorder.setVideoFrameRate(24);
    mMediaRecorder.setVideoSize(1280, 720);
    mMediaRecorder.setVideoEncodingBitRate(3000000);
    mMediaRecorder.setAudioEncodingBitRate(8000);

    mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
    mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);

    try {
        mMediaRecorder.prepare();
    } catch (IllegalStateException e) {
        // This is thrown if the previous calls are not called with the
        // proper order
        e.printStackTrace();
    }

    mInitSuccesful = true;
}

private File initFile() {
    // File dir = new
    // File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
    // this
    File dir = new File(Environment.getExternalStorageDirectory(), this
            .getClass().getPackage().getName());


    if (!dir.exists() && !dir.mkdirs()) {
        Log.wtf(TAG,
                "Failed to create storage directory: "
                        + dir.getAbsolutePath());
        Toast.makeText(Video.this, "not record", Toast.LENGTH_SHORT);
        file = null;
    } else {
        file = new File(dir.getAbsolutePath(), new SimpleDateFormat(
                "'IMG_'yyyyMMddHHmmss'.mp4'").format(new Date()));
    }
    return file;
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
    try {
        if (!mInitSuccesful)
            initRecorder(mHolder.getSurface());
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

private void shutdown() {
    // Release MediaRecorder and especially the Camera as it's a shared
    // object that can be used by other applications
    mMediaRecorder.reset();
    mMediaRecorder.release();
    mCamera.release();

    // once the objects have been released they can't be reused
    mMediaRecorder = null;
    mCamera = null;
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    shutdown();
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
        int height) {
    // TODO Auto-generated method stub

}

@Override
public void onClick(View v) {
    // TODO Auto-generated method stub

}

}

MediaMetadataRetriever Class

public class MediaMetadataRetriever {

 static {
        System.loadLibrary("media_jni");
        native_init();
    }

    // The field below is accessed by native methods
    @SuppressWarnings("unused")
    private int mNativeContext;

    public MediaMetadataRetriever() {
        native_setup();
    }

    /**
     * Call this method before setDataSource() so that the mode becomes
     * effective for subsequent operations. This method can be called only once
     * at the beginning if the intended mode of operation for a
     * MediaMetadataRetriever object remains the same for its whole lifetime,
     * and thus it is unnecessary to call this method each time setDataSource()
     * is called. If this is not never called (which is allowed), by default the
     * intended mode of operation is to both capture frame and retrieve meta
     * data (i.e., MODE_GET_METADATA_ONLY | MODE_CAPTURE_FRAME_ONLY).
     * Often, this may not be what one wants, since doing this has negative
     * performance impact on execution time of a call to setDataSource(), since
     * both types of operations may be time consuming.
     * 
     * @param mode The intended mode of operation. Can be any combination of 
     * MODE_GET_METADATA_ONLY and MODE_CAPTURE_FRAME_ONLY:
     * 1. MODE_GET_METADATA_ONLY & MODE_CAPTURE_FRAME_ONLY: 
     *    For neither frame capture nor meta data retrieval
     * 2. MODE_GET_METADATA_ONLY: For meta data retrieval only
     * 3. MODE_CAPTURE_FRAME_ONLY: For frame capture only
     * 4. MODE_GET_METADATA_ONLY | MODE_CAPTURE_FRAME_ONLY: 
     *    For both frame capture and meta data retrieval
     */
    public native void setMode(int mode);

    /**
     * @return the current mode of operation. A negative return value indicates
     * some runtime error has occurred.
     */
    public native int getMode();

    /**
     * Sets the data source (file pathname) to use. Call this
     * method before the rest of the methods in this class. This method may be
     * time-consuming.
     * 
     * @param path The path of the input media file.
     * @throws IllegalArgumentException If the path is invalid.
     */
    public native void setDataSource(String path) throws IllegalArgumentException;

    /**
     * Sets the data source (FileDescriptor) to use.  It is the caller's
     * responsibility to close the file descriptor. It is safe to do so as soon
     * as this call returns. Call this method before the rest of the methods in
     * this class. This method may be time-consuming.
     * 
     * @param fd the FileDescriptor for the file you want to play
     * @param offset the offset into the file where the data to be played starts,
     * in bytes. It must be non-negative
     * @param length the length in bytes of the data to be played. It must be
     * non-negative.
     * @throws IllegalArgumentException if the arguments are invalid
     */
    public native void setDataSource(FileDescriptor fd, long offset, long length)
            throws IllegalArgumentException;

    /**
     * Sets the data source (FileDescriptor) to use. It is the caller's
     * responsibility to close the file descriptor. It is safe to do so as soon
     * as this call returns. Call this method before the rest of the methods in
     * this class. This method may be time-consuming.
     * 
     * @param fd the FileDescriptor for the file you want to play
     * @throws IllegalArgumentException if the FileDescriptor is invalid
     */
    public void setDataSource(FileDescriptor fd)
            throws IllegalArgumentException {
        // intentionally less than LONG_MAX
        setDataSource(fd, 0, 0x7ffffffffffffffL);
    }

    /**
     * Sets the data source as a content Uri. Call this method before 
     * the rest of the methods in this class. This method may be time-consuming.
     * 
     * @param context the Context to use when resolving the Uri
     * @param uri the Content URI of the data you want to play
     * @throws IllegalArgumentException if the Uri is invalid
     * @throws SecurityException if the Uri cannot be used due to lack of
     * permission.
     */
    public void setDataSource(Context context, Uri uri)
        throws IllegalArgumentException, SecurityException {
        if (uri == null) {
            throw new IllegalArgumentException();
        }

        String scheme = uri.getScheme();
        if(scheme == null || scheme.equals("file")) {
            setDataSource(uri.getPath());
            return;
        }

        AssetFileDescriptor fd = null;
        try {
            ContentResolver resolver = context.getContentResolver();
            try {
                fd = resolver.openAssetFileDescriptor(uri, "r");
            } catch(FileNotFoundException e) {
                throw new IllegalArgumentException();
            }
            if (fd == null) {
                throw new IllegalArgumentException();
            }
            FileDescriptor descriptor = fd.getFileDescriptor();
            if (!descriptor.valid()) {
                throw new IllegalArgumentException();
            }
            // Note: using getDeclaredLength so that our behavior is the same
            // as previous versions when the content provider is returning
            // a full file.
            if (fd.getDeclaredLength() < 0) {
                setDataSource(descriptor);
            } else {
                setDataSource(descriptor, fd.getStartOffset(), fd.getDeclaredLength());
            }
            return;
        } catch (SecurityException ex) {
        } finally {
            try {
                if (fd != null) {
                    fd.close();
                }
            } catch(IOException ioEx) {
            }
        }
        setDataSource(uri.toString());
    }

    /**
     * Call this method after setDataSource(). This method retrieves the 
     * meta data value associated with the keyCode.
     * 
     * The keyCode currently supported is listed below as METADATA_XXX
     * constants. With any other value, it returns a null pointer.
     * 
     * @param keyCode One of the constants listed below at the end of the class.
     * @return The meta data value associate with the given keyCode on success; 
     * null on failure.
     */
    public native String extractMetadata(int keyCode);

    /**
     * Call this method after setDataSource(). This method finds a
     * representative frame if successful and returns it as a bitmap. This is
     * useful for generating a thumbnail for an input media source.
     * 
     * @return A Bitmap containing a representative video frame, which 
     *         can be null, if such a frame cannot be retrieved.
     */
    public native Bitmap captureFrame();

    /**
     * Call this method after setDataSource(). This method finds the optional
     * graphic or album art associated (embedded or external url linked) the 
     * related data source.
     * 
     * @return null if no such graphic is found.
     */
    public native byte[] extractAlbumArt();

    /**
     * Call it when one is done with the object. This method releases the memory
     * allocated internally.
     */
    public native void release();
    private native void native_setup();
    private static native void native_init();

    private native final void native_finalize();

    @Override
    protected void finalize() throws Throwable {
        try {
            native_finalize();
        } finally {
            super.finalize();
        }
    }

    public static final int MODE_GET_METADATA_ONLY  = 0x01;
    public static final int MODE_CAPTURE_FRAME_ONLY = 0x02;

    /*
     * Do not change these values without updating their counterparts
     * in include/media/mediametadataretriever.h!
     */
    public static final int METADATA_KEY_CD_TRACK_NUMBER = 0;
    public static final int METADATA_KEY_ALBUM           = 1;
    public static final int METADATA_KEY_ARTIST          = 2;
    public static final int METADATA_KEY_AUTHOR          = 3;
    public static final int METADATA_KEY_COMPOSER        = 4;
    public static final int METADATA_KEY_DATE            = 5;
    public static final int METADATA_KEY_GENRE           = 6;
    public static final int METADATA_KEY_TITLE           = 7;
    public static final int METADATA_KEY_YEAR            = 8;
    public static final int METADATA_KEY_DURATION        = 9;
    public static final int METADATA_KEY_NUM_TRACKS      = 10;
    public static final int METADATA_KEY_IS_DRM_CRIPPLED = 11;
    public static final int METADATA_KEY_CODEC           = 12;
    public static final int METADATA_KEY_RATING          = 13;
    public static final int METADATA_KEY_COMMENT         = 14;
    public static final int METADATA_KEY_COPYRIGHT       = 15;
    public static final int METADATA_KEY_BIT_RATE        = 16;
    public static final int METADATA_KEY_FRAME_RATE      = 17;
    public static final int METADATA_KEY_VIDEO_FORMAT    = 18;
    public static final int METADATA_KEY_VIDEO_HEIGHT    = 19;
    public static final int METADATA_KEY_VIDEO_WIDTH     = 20;
    public static final int METADATA_KEY_WRITER          = 21;
    // Add more here...
}

สวัสดี ฉันกำลังพยายามใช้รหัสของคุณในโครงการของฉัน แต่ไม่เคยเรียกใช้ฟังก์ชั่น surfaceCreated
REJH

@REJH มันถูกเรียกเมื่อสร้างพื้นผิวขึ้นเนื่องจากบรรทัดนี้mHolder.addCallback(this);แนบคลาสกิจกรรมเพื่อจัดการเหตุการณ์บนพื้นผิว อาจเป็น IDE ของคุณไม่สามารถรับสายนี้ แต่ควรจะเรียก
vp_arth

3

ลองดูโค้ดตัวอย่างกล้องตัวอย่างนี้, CameraPreview. สิ่งนี้จะช่วยคุณในการพัฒนารหัสการบันทึกวิดีโอสำหรับการแสดงตัวอย่างวิดีโอสร้างMediaRecorderวัตถุและตั้งค่าพารามิเตอร์การบันทึกวิดีโอ


2
URL ที่ถูกต้องคือdeveloper.android.com/resources/samples/ApiDemos/src/com/... มันอาจมีการเปลี่ยนแปลงตั้งแต่ piyshnp โพสต์คำตอบของเขา
Bilthon

และอีกครั้ง: การควบคุมกล้อง
vp_arth

3

เพื่อประโยชน์ของผู้ค้นหาตัวอย่างนี้จะให้ภาพตัวอย่างที่ใช้งานอยู่พร้อมปุ่มเริ่ม / หยุดเพื่อบันทึก มันถูกดัดแปลงจากบล็อก android นี้และดูเหมือนว่าน่าเชื่อถือพอสมควร

คลาส java (VideoWithSurfaceVw)

package <<your packagename here>>;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

import android.app.Activity;
import android.content.Context;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.Toast;

public class VideoWithSurfaceVw extends Activity{

    // Adapted from http://sandyandroidtutorials.blogspot.co.uk/2013/05/android-video-capture-tutorial.html


    private Camera myCamera;
    private MyCameraSurfaceView myCameraSurfaceView;
    private MediaRecorder mediaRecorder;

    Button myButton;
    SurfaceHolder surfaceHolder;
    boolean recording;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        recording = false;

        setContentView(R.layout.activity_video_with_surface_vw);

        //Get Camera for preview
        myCamera = getCameraInstance();
        if(myCamera == null){
            Toast.makeText(VideoWithSurfaceVw.this,
                    "Fail to get Camera",
                    Toast.LENGTH_LONG).show();
        }

        myCameraSurfaceView = new MyCameraSurfaceView(this, myCamera);
        FrameLayout myCameraPreview = (FrameLayout)findViewById(R.id.videoview);
        myCameraPreview.addView(myCameraSurfaceView);

        myButton = (Button)findViewById(R.id.mybutton);
        myButton.setOnClickListener(myButtonOnClickListener);
    }

    Button.OnClickListener myButtonOnClickListener
            = new Button.OnClickListener(){

        @Override
        public void onClick(View v) {
            // TODO Auto-generated method stub

            try{
                if(recording){
                    // stop recording and release camera
                    mediaRecorder.stop();  // stop the recording
                    releaseMediaRecorder(); // release the MediaRecorder object

                    //Exit after saved
                    //finish();
                    myButton.setText("REC");
                    recording = false;
                }else{

                    //Release Camera before MediaRecorder start
                    releaseCamera();

                    if(!prepareMediaRecorder()){
                        Toast.makeText(VideoWithSurfaceVw.this,
                                "Fail in prepareMediaRecorder()!\n - Ended -",
                                Toast.LENGTH_LONG).show();
                        finish();
                    }

                    mediaRecorder.start();
                    recording = true;
                    myButton.setText("STOP");
                }
            }catch (Exception ex){
                ex.printStackTrace();
            }
        }};

    private Camera getCameraInstance(){
        // TODO Auto-generated method stub
        Camera c = null;
        try {
            c = Camera.open(); // attempt to get a Camera instance
        }
        catch (Exception e){
            // Camera is not available (in use or does not exist)
        }
        return c; // returns null if camera is unavailable
    }

    private String getFileName_CustomFormat() {
        SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd HH_mm_ss");
        Date now = new Date();
        String strDate = sdfDate.format(now);
        return strDate;
    }


    private boolean prepareMediaRecorder(){
        myCamera = getCameraInstance();
        mediaRecorder = new MediaRecorder();

        myCamera.unlock();
        mediaRecorder.setCamera(myCamera);

        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

        mediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));



        mediaRecorder.setOutputFile("/sdcard/" + getFileName_CustomFormat() + ".mp4");
        //mediaRecorder.setOutputFile("/sdcard/myvideo1.mp4");
        mediaRecorder.setMaxDuration(60000); // Set max duration 60 sec.
        mediaRecorder.setMaxFileSize(50000000); // Set max file size 50M

        mediaRecorder.setPreviewDisplay(myCameraSurfaceView.getHolder().getSurface());

        try {
            mediaRecorder.prepare();
        } catch (IllegalStateException e) {
            releaseMediaRecorder();
            return false;
        } catch (IOException e) {
            releaseMediaRecorder();
            return false;
        }
        return true;

    }

    @Override
    protected void onPause() {
        super.onPause();
        releaseMediaRecorder();       // if you are using MediaRecorder, release it first
        releaseCamera();              // release the camera immediately on pause event
    }

    private void releaseMediaRecorder(){
        if (mediaRecorder != null) {
            mediaRecorder.reset();   // clear recorder configuration
            mediaRecorder.release(); // release the recorder object
            mediaRecorder = new MediaRecorder();
            myCamera.lock();           // lock camera for later use
        }
    }

    private void releaseCamera(){
        if (myCamera != null){
            myCamera.release();        // release the camera for other applications
            myCamera = null;
        }
    }

    public class MyCameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback{

        private SurfaceHolder mHolder;
        private Camera mCamera;

        public MyCameraSurfaceView(Context context, Camera camera) {
            super(context);
            mCamera = camera;

            // Install a SurfaceHolder.Callback so we get notified when the
            // underlying surface is created and destroyed.
            mHolder = getHolder();
            mHolder.addCallback(this);
            // deprecated setting, but required on Android versions prior to 3.0
            mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int weight,
                                   int height) {
            // If your preview can change or rotate, take care of those events here.
            // Make sure to stop the preview before resizing or reformatting it.

            if (mHolder.getSurface() == null){
                // preview surface does not exist
                return;
            }

            // stop preview before making changes
            try {
                mCamera.stopPreview();
            } catch (Exception e){
                // ignore: tried to stop a non-existent preview
            }

            // make any resize, rotate or reformatting changes here

            // start preview with new settings
            try {
                mCamera.setPreviewDisplay(mHolder);
                mCamera.startPreview();

            } catch (Exception e){
            }
        }

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            // TODO Auto-generated method stub
            // The Surface has been created, now tell the camera where to draw the preview.
            try {
                mCamera.setPreviewDisplay(holder);
                mCamera.startPreview();
            } catch (IOException e) {
            }
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            // TODO Auto-generated method stub

        }
    }
}

กิจกรรม (activity_video_with_surface_vw)

<RelativeLayout android:id="@+id/surface_camera"     
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_centerInParent="true"
android:layout_weight="1"
>

<RelativeLayout
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <FrameLayout
        android:id="@+id/videoview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"/>
    <Button
        android:id="@+id/mybutton"
        android:layout_width="100dp"
        android:layout_height="50dp"
        android:layout_centerHorizontal="true"
        android:layout_alignParentBottom="true"
        android:text="REC"
        android:textSize="12dp"/>
</RelativeLayout>

</RelativeLayout>

การรันโปรเจ็กต์ใน Android Studio จะสร้างข้อความแสดงข้อผิดพลาดต่อไปนี้ในไฟล์. xml: ข้อผิดพลาด: (29) ข้อผิดพลาดในการแยกวิเคราะห์ XML: ไม่พบองค์ประกอบที่ 29 คือบรรทัดที่สอดคล้องกับ </RelativeLayout>
bergercookie

@bergercookie - คุณพบ / แก้ไขปัญหาหรือไม่ ฉันยกสิ่งนี้จากรหัสการทำงานของฉัน (โดยใช้ Android Studio) อย่าลังเลที่จะแก้ไขโพสต์ของฉันหากมีปัญหา
HockeyJ

1
@bergercookie - ตัวอย่าง xml ขาดแท็ก RelativeLayout ที่ปิดไป ฉันแก้ไขการจัดรูปแบบแล้วและตอนนี้แท็กปิดจะแสดงอย่างถูกต้อง
mjp66

1

ในฐานะที่เป็นบันทึกด้าน - ดูเหมือนจะมีข้อผิดพลาดใน Android API หรือเอกสารผิดพลาดหรือบางทีฉันแค่โง่ธรรมดา เอกสาร google ระบุอย่างชัดเจนดังต่อไปนี้:

หมายเหตุ: เริ่มต้นด้วย Android 4.0 (ระดับ API 14) การเรียก Camera.lock () และ Camera.unlock () จะได้รับการจัดการให้คุณโดยอัตโนมัติ

ดู: http://developer.android.com/guide/topics/media/camera.html

นี่ดูเหมือนจะไม่เป็นอย่างนั้น!

หลังจากที่ต้องต่อสู้กับวันวรรณกรรมโดยไม่ประสบความสำเร็จและปัญหาเล็ก ๆ มากมายเช่น "ไม่สามารถเริ่มต้น" ข้อผิดพลาดได้ฉันตัดสินใจที่จะใช้การล็อกและ BAM ด้วยตนเอง! ทุกอย่างทำงานได้ดี

ฉันใช้ตัวจำลองจีโนมสำหรับอุปกรณ์ 4.1.1 ด้วย min sdk เท่ากับ 14


1

ตัวอย่างด้านบนจะใช้งานได้หากคุณใช้กล้องด้านหลัง หากคุณใช้กล้องหน้าคุณจะต้องปรับเปลี่ยนบางสิ่ง:

ก่อนอื่นคุณจะต้องเพิ่มการอนุญาตใหม่ในไฟล์ Manifest

<uses-feature android:name="android.hardware.camera.front" android:required="false" />

ในinitRecorderวิธีการของคุณแทน

CamcorderProfile cpHigh = CamcorderProfile
                .get(CamcorderProfile.QUALITY_HIGH);
recorder.setProfile(cpHigh);

คุณต้องใช้:

CamcorderProfile profile = CamcorderProfile.get(Camera.CameraInfo.CAMERA_FACING_FRONT, CamcorderProfile.QUALITY_LOW);
recorder.setProfile(profile);

เพราะCamcorderProfile.QUALITY_HIGHสงวนไว้สำหรับกล้องด้านหลัง

คุณจะต้องตั้งค่าขนาดวิดีโอสำหรับสื่อกลางตามที่อยู่ในมุมมองพื้นผิวของคุณ

นี่คือตัวอย่างเต็มรูปแบบของการบันทึกวิดีโอจากกล้องหน้าด้วยหน้าจอตัวอย่างขนาดเล็ก:

Android.manifest

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.front" android:required="false" />

activity_camera.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="CameraActivity">

    <SurfaceView
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:id="@+id/surfaceView"/>

    <Button
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:text="REC"
        android:id="@+id/btnRecord"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="25dp" />
</RelativeLayout>

CameraActivity.java

public class SongVideoActivity extends BaseActivity implements SurfaceHolder.Callback {

    private int mCameraContainerWidth = 0;
    private SurfaceView mSurfaceView = null;
    private SurfaceHolder mSurfaceHolder = null;

    private Camera mCamera = null;
    private boolean mIsRecording = false;

    private int mPreviewHeight;
    private int mPreviewWidth;

    MediaRecorder mRecorder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_song_video);

        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread thread, Throwable ex) {
                releaseMediaRecorder();
                releaseCamera();
            }
        });

        mCamera = getCamera();

        //camera preview
        mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView);
        mSurfaceHolder = mSurfaceView.getHolder();
        mSurfaceHolder.addCallback(this);
        // deprecated setting, but required on Android versions prior to 3.0
        mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

        mCameraContainerWidth = mSurfaceView.getLayoutParams().width;

        findViewById(R.id.btnRecord).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mIsRecording) {
                    stopRecording();
                } else {
                    // initialize video camera
                    if (prepareVideoRecorder()) {

                        // Camera is available and unlocked, MediaRecorder is prepared,
                        // now you can start recording
                        mRecorder.start();

                        // inform the user that recording has started
                        Toast.makeText(getApplicationContext(), "Started recording", Toast.LENGTH_SHORT).show();
                        mIsRecording = true;
                    } else {
                        // prepare didn't work, release the camera
                        releaseMediaRecorder();
                        // inform user
                    }
                }
            }
        });
    }

    private void stopRecording() {
        mRecorder.stop();  // stop the recording
        releaseMediaRecorder(); // release the MediaRecorder object
        mCamera.lock();         // take camera access back from MediaRecorder

        // inform the user that recording has stopped
        Toast.makeText(this, "Recording complete", Toast.LENGTH_SHORT).show();
        mIsRecording = false;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        releaseMediaRecorder();       // if you are using MediaRecorder, release it first
        releaseCamera();              // release the camera immediately on pause event
    }

    private Camera getCamera() {

        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
        for (int camIdx = 0; camIdx < Camera.getNumberOfCameras(); camIdx++) {
            Camera.getCameraInfo(camIdx, cameraInfo);
            if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                try {
                    return mCamera = Camera.open(camIdx);
                } catch (RuntimeException e) {
                    Log.e("cameras", "Camera failed to open: " + e.getLocalizedMessage());
                }
            }
        }
        return null;
    }

    @Override
    protected void onPause() {
        super.onPause();
        releaseMediaRecorder();       // if you are using MediaRecorder, release it first
        releaseCamera();              // release the camera immediately on pause event
    }

    private Camera.Size getBestPreviewSize(Camera.Parameters parameters) {
        Camera.Size result=null;

        for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
            if(size.width < size.height) continue; //we are only interested in landscape variants

            if (result == null) {
                result = size;
            }
            else {
                int resultArea = result.width*result.height;
                int newArea = size.width*size.height;

                if (newArea > resultArea) {
                    result = size;
                }
            }
        }

        return(result);
    }

    private boolean prepareVideoRecorder(){
        mRecorder = new MediaRecorder();

        // Step 1: Unlock and set camera to MediaRecorder
        mCamera.unlock();
        mRecorder.setCamera(mCamera);

        // Step 2: Set sources
        mRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
        mRecorder.setVideoSource(MediaRecorder.VideoSource.DEFAULT);
        //recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);

        // Step 3: Set a CamcorderProfile (requires API Level 8 or higher)
        // Customise your profile based on a pre-existing profile
        CamcorderProfile profile = CamcorderProfile.get(Camera.CameraInfo.CAMERA_FACING_FRONT, CamcorderProfile.QUALITY_LOW);
        mRecorder.setProfile(profile);

        // Step 4: Set output file
        mRecorder.setOutputFile(new File(getFilesDir(), "movie-" + UUID.randomUUID().toString()).getAbsolutePath());
        //recorder.setMaxDuration(50000); // 50 seconds
        //recorder.setMaxFileSize(500000000); // Approximately 500 megabytes

        mRecorder.setVideoSize(mPreviewWidth, mPreviewHeight);

        // Step 5: Set the preview output
        mRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());

        // Step 6: Prepare configured MediaRecorder
        try {
            mRecorder.prepare();
        } catch (IllegalStateException e) {
            Toast.makeText(getApplicationContext(), "exception: " + e.getMessage(), Toast.LENGTH_LONG).show();
            releaseMediaRecorder();
            return false;
        } catch (IOException e) {
            Toast.makeText(getApplicationContext(), "exception: " + e.getMessage(), Toast.LENGTH_LONG).show();
            releaseMediaRecorder();
            return false;
        }
        return true;
    }

    private void releaseMediaRecorder(){
        if (mRecorder != null) {
            mRecorder.reset();   // clear recorder configuration
            mRecorder.release(); // release the recorder object
            mRecorder = null;
            mCamera.lock();           // lock camera for later use
        }
    }

    private void releaseCamera(){
        if (mCamera != null){
            mCamera.release();        // release the camera for other applications
            mCamera = null;
        }
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // The Surface has been created, now tell the camera where to draw the preview.
            Camera.Parameters parameters = mCamera.getParameters();
            parameters.setRecordingHint(true);
            Camera.Size size = getBestPreviewSize(parameters);
        mCamera.setParameters(parameters);

            //resize the view to the specified surface view width in layout
            int newHeight = size.height / (size.width / mCameraContainerWidth);
            mSurfaceView.getLayoutParams().height = newHeight;
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        mPreviewHeight = mCamera.getParameters().getPreviewSize().height;
        mPreviewWidth = mCamera.getParameters().getPreviewSize().width;

        mCamera.stopPreview();
        try {
            mCamera.setPreviewDisplay(mSurfaceHolder);
        } catch (IOException e) {
            e.printStackTrace();
        }
        mCamera.startPreview();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (mIsRecording) {
            stopRecording();
        }

        releaseMediaRecorder();
        releaseCamera();
    }
}

สิ่งนี้ใช้ไม่ได้ แอปของฉันหยุดทำงานโดยการปิด VM
Pink Jazz

Works! แต่วิดีโอที่บันทึกไว้กลับหัวกลับหาง นอกจากนี้เมื่อฉันพยายามที่จะทำ match_parent กับความกว้างภาพตัวอย่างจะไม่ปรากฏ : /
M. Usman Khan

สำหรับการตั้งค่าความกว้างและความสูงของหน้าตัวอย่างแสดงตัวอย่างเป็น wrap_content
M. Usman Khan

ฉันจะตั้งค่าตัวแปร PORTRAIT ได้อย่างไร
Rozina

1

จนถึงเดือนธันวาคม 2560 มีการอัพเดทบางอย่างเช่นการใช้android.hardware.Cameraเลิกใช้แล้วในตอนนี้ ในขณะที่รุ่นใหม่มาพร้อมกับสิ่งที่มีประโยชน์เช่นandroid.hardware.camera2CameraManager

ฉันชอบตัวอย่างนี้มากซึ่งใช้ประโยชน์จาก API ปัจจุบันนี้และใช้งานได้อย่างมีเสน่ห์: https://github.com/googlesamples/android-Camera2Video

นอกจากนี้ยังรวมถึงการขอสิทธิ์การใช้งานที่จำเป็นเมื่อเริ่มต้นและแสดงตัวอย่างวิดีโอก่อนเริ่มการบันทึกวิดีโอ

(นอกจากนี้ฉันพบว่ารหัสสวยงามจริงๆ (และนี่หายากมากสำหรับฉัน ^^) แต่นั่นเป็นเพียงความเห็นส่วนตัวของฉัน)


ไม่ทำงานกับซัมซุง J2 :( ไฟล์วิดีโอที่ได้ไม่สามารถเล่นได้
Sam Sch

0

แทนที่จะเขียนโค้ดจากแบบร่างคุณสามารถใช้ไลบรารีบน GitHub ตัวอย่างเช่น: https://github.com/CameraKit/camerakit-android (หรือhttps://github.com/google/cameraviewหรือhttps://github.com/hujiaweibujidao/CameraViewเป็นต้น) จากนั้นคุณจะต้อง:

private CameraKitView cameraKitView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    cameraKitView = findViewById(R.id.camera);
}

@Override
protected void onStart() {
    super.onStart();
    cameraKitView.onStart();
}

@Override
protected void onResume() {
    super.onResume();
    cameraKitView.onResume();
}

@Override
protected void onPause() {
    cameraKitView.onPause();
    super.onPause();
}

@Override
protected void onStop() {
    cameraKitView.onStop();
    super.onStop();
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    cameraKitView.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

คุณทดสอบสิ่งนี้หรือไม่?
pookie

@ pookie ฉันคิดว่าใช่ แต่ลืมไปเพราะมันผ่านมา 1.5 ปีแล้ว ที่จริงแล้วคุณอาจไปดูลิงก์ repo ของ GitHub เหล่านั้นเนื่องจากพวกเขาจะมี "quickstart" ซึ่งคุณสามารถติดตามได้
ch271828n

เยี่ยมมากขอบคุณ :)
pookie

@pookie ถ้าคุณคิดว่ามันมีประโยชน์โปรดให้ upvote :)
ch271828n
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.