From 6fb64eb227776205b6a3dbef29a4b47fd898824a Mon Sep 17 00:00:00 2001 From: hudi Date: Mon, 21 May 2018 21:02:49 +0800 Subject: [PATCH] commit --- .../com/hd/screen/capture/MainActivity.java | 36 +- app/src/main/res/layout/activity_main.xml | 29 +- .../com/hd/screencapture/ScreenCapture.java | 38 +- .../callback/RecorderCallback.java | 18 + .../callback/ScreenCaptureStreamCallback.java | 15 + .../screencapture/capture/AudioRecorder.java | 41 ++ .../hd/screencapture/capture/Recorder.java | 29 ++ .../screencapture/capture/VideoRecorder.java | 134 ++++++ .../config/ScreenCaptureConfig.java | 18 + .../hd/screencapture/config/VideoConfig.java | 2 + .../hd/screencapture/help/ExecutorUtil.java | 51 +++ .../help/ScreenCaptureFragment.java | 35 +- .../help/ScreenCaptureRecorder.java | 418 ++++++++++++++---- .../observer/CaptureObserver.java | 38 +- .../observer/ScreenCaptureObserver.java | 1 + 15 files changed, 775 insertions(+), 128 deletions(-) create mode 100644 screencapture/src/main/java/com/hd/screencapture/callback/RecorderCallback.java create mode 100644 screencapture/src/main/java/com/hd/screencapture/callback/ScreenCaptureStreamCallback.java create mode 100644 screencapture/src/main/java/com/hd/screencapture/help/ExecutorUtil.java diff --git a/app/src/main/java/com/hd/screen/capture/MainActivity.java b/app/src/main/java/com/hd/screen/capture/MainActivity.java index 74631d9..40f2c9c 100644 --- a/app/src/main/java/com/hd/screen/capture/MainActivity.java +++ b/app/src/main/java/com/hd/screen/capture/MainActivity.java @@ -1,14 +1,17 @@ package com.hd.screen.capture; +import android.annotation.SuppressLint; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.text.format.DateUtils; import android.util.Log; import android.view.View; +import android.widget.TextView; import android.widget.Toast; import com.hd.screencapture.ScreenCapture; -import com.hd.screencapture.callback.ScreenCaptureCallback; +import com.hd.screencapture.callback.ScreenCaptureStreamCallback; import com.hd.screencapture.config.AudioConfig; import com.hd.screencapture.config.ScreenCaptureConfig; import com.hd.screencapture.config.VideoConfig; @@ -18,21 +21,28 @@ /** * Created by hd on 2018/5/14 . */ -public class MainActivity extends AppCompatActivity implements ScreenCaptureCallback { +public class MainActivity extends AppCompatActivity implements ScreenCaptureStreamCallback { private ScreenCapture screenCapture; private boolean isRunning; + private TextView tvTime,tvVideoHeaderData,tvVideoData,tvAudioData; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + tvTime=findViewById(R.id.tvTime); + tvVideoHeaderData=findViewById(R.id.tvVideoHeaderData); + tvVideoData=findViewById(R.id.tvVideoData); + tvAudioData=findViewById(R.id.tvAudioData); init(); } private void init() { ScreenCaptureConfig captureConfig = new ScreenCaptureConfig.Builder()// + .setAllowLog(BuildConfig.DEBUG) .setVideoConfig(VideoConfig.initDefaultConfig(this))// .setAudioConfig(AudioConfig.initDefaultConfig())// .setCaptureCallback(this)// @@ -64,8 +74,28 @@ public void captureState(ScreenCaptureState state) { runOnUiThread(() -> Toast.makeText(MainActivity.this, "capture state ==>" + state, Toast.LENGTH_SHORT).show()); } + @SuppressLint("SetTextI18n") @Override public void captureTime(long time) { - Log.d("tag", "capture time ==>" + time + "===" + DateUtils.formatElapsedTime(time)); + runOnUiThread(() -> tvTime.setText("capture time ==>"+ DateUtils.formatElapsedTime(time))); + } + + @SuppressLint("SetTextI18n") + @Override + public void videoHeaderByte(@NonNull byte[] sps, @NonNull byte[] pps) { + runOnUiThread(() -> tvVideoHeaderData.setText("video header byte length ==> sps len: " + sps.length +", pps len : "+ pps.length)); + } + + @SuppressLint("SetTextI18n") + @Override + public void videoContentByte(@NonNull byte[] content) { + runOnUiThread(() -> tvVideoData.setText("video content byte len ==> " + content.length)); + + } + + @SuppressLint("SetTextI18n") + @Override + public void audioContentByte(@NonNull byte[] content) { + runOnUiThread(() -> tvAudioData.setText("audio content byte len ==> " + content.length)); } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index fbbca81..a61bede 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,7 +1,6 @@ + + + + + + + + \ No newline at end of file diff --git a/screencapture/src/main/java/com/hd/screencapture/ScreenCapture.java b/screencapture/src/main/java/com/hd/screencapture/ScreenCapture.java index f3a99af..281688d 100644 --- a/screencapture/src/main/java/com/hd/screencapture/ScreenCapture.java +++ b/screencapture/src/main/java/com/hd/screencapture/ScreenCapture.java @@ -16,6 +16,7 @@ import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicBoolean; /** @@ -33,8 +34,8 @@ public static ScreenCapture with(@NonNull AppCompatActivity activity) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { throw new RuntimeException("the sdk version less than 21 equipment does not provide this function !"); } - if(!Utils.isExternalStorageReady()){ - Log.e(TAG,"current no storage space"); + if (!Utils.isExternalStorageReady()) { + Log.e(TAG, "current no storage space"); } return new ScreenCapture(activity); } @@ -43,6 +44,8 @@ public static ScreenCapture with(@NonNull AppCompatActivity activity) { private CaptureObserver observer; + protected AtomicBoolean capture = new AtomicBoolean(false); + private ScreenCapture(@NonNull AppCompatActivity activity) { //add lifecycle observer observer = new ScreenCaptureObserver(this); @@ -78,24 +81,37 @@ public ScreenCapture setConfig(@NonNull ScreenCaptureConfig config) { return this; } + public boolean isRunning(){ + return capture.get(); + } + public void startCapture() { startCapture(-1); } public void startCapture(long duration) { - screenCaptureFragment.startCapture(); - if (duration > 0) { - new Timer().schedule(new TimerTask() { - @Override - public void run() { - stopCapture(); - } - }, duration); + if (!isRunning()) { + capture.set(true); + screenCaptureFragment.startCapture(); + if (duration > 0) { + new Timer().schedule(new TimerTask() { + @Override + public void run() { + stopCapture(); + } + }, duration); + } + } else { + Log.e(TAG, "capturing !!!"); } } public void stopCapture() { - screenCaptureFragment.stopCapture(); + if (isRunning()) { + screenCaptureFragment.stopCapture(); + } else { + Log.e(TAG, "stop capture always"); + } } } diff --git a/screencapture/src/main/java/com/hd/screencapture/callback/RecorderCallback.java b/screencapture/src/main/java/com/hd/screencapture/callback/RecorderCallback.java new file mode 100644 index 0000000..a55100a --- /dev/null +++ b/screencapture/src/main/java/com/hd/screencapture/callback/RecorderCallback.java @@ -0,0 +1,18 @@ +package com.hd.screencapture.callback; + +import android.media.MediaCodec; +import android.media.MediaFormat; + +/** + * Created by hd on 2018/5/20 . + */ +public interface RecorderCallback { + + void onInputBufferAvailable(int index); + + void onOutputFormatChanged(MediaFormat format); + + void onOutputBufferAvailable(int index, MediaCodec.BufferInfo info); + + void onError(Exception exception); +} diff --git a/screencapture/src/main/java/com/hd/screencapture/callback/ScreenCaptureStreamCallback.java b/screencapture/src/main/java/com/hd/screencapture/callback/ScreenCaptureStreamCallback.java new file mode 100644 index 0000000..c7e46d5 --- /dev/null +++ b/screencapture/src/main/java/com/hd/screencapture/callback/ScreenCaptureStreamCallback.java @@ -0,0 +1,15 @@ +package com.hd.screencapture.callback; + +import android.support.annotation.NonNull; + +/** + * Created by hd on 2018/5/21 . + */ +public interface ScreenCaptureStreamCallback extends ScreenCaptureCallback { + + void videoHeaderByte(@NonNull byte[] sps,@NonNull byte[] pps); + + void videoContentByte(@NonNull byte[] content); + + void audioContentByte(@NonNull byte[] content); +} diff --git a/screencapture/src/main/java/com/hd/screencapture/capture/AudioRecorder.java b/screencapture/src/main/java/com/hd/screencapture/capture/AudioRecorder.java index 9b29891..461b083 100644 --- a/screencapture/src/main/java/com/hd/screencapture/capture/AudioRecorder.java +++ b/screencapture/src/main/java/com/hd/screencapture/capture/AudioRecorder.java @@ -1,7 +1,48 @@ package com.hd.screencapture.capture; +import android.annotation.TargetApi; +import android.os.Build; +import android.support.annotation.NonNull; + +import com.hd.screencapture.callback.RecorderCallback; +import com.hd.screencapture.config.ScreenCaptureConfig; +import com.hd.screencapture.observer.CaptureObserver; + +import java.nio.ByteBuffer; + /** * Created by hd on 2018/5/20 . */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class AudioRecorder extends Recorder { + + public AudioRecorder(@NonNull CaptureObserver observer, @NonNull ScreenCaptureConfig config,// + @NonNull RecorderCallback callback) { + super(observer, config, callback); + } + + @Override + public boolean prepare() { + return true; + } + + @Override + public boolean record() { + return true; + } + + @Override + public void release() { + + } + + public void releaseOutputBuffer(int index) { + } + + + public ByteBuffer getOutputBuffer(int index) { +// return mEncoder.getOutputBuffer(index); + return null; + } + } diff --git a/screencapture/src/main/java/com/hd/screencapture/capture/Recorder.java b/screencapture/src/main/java/com/hd/screencapture/capture/Recorder.java index 1c9b3cd..748d3f8 100644 --- a/screencapture/src/main/java/com/hd/screencapture/capture/Recorder.java +++ b/screencapture/src/main/java/com/hd/screencapture/capture/Recorder.java @@ -1,7 +1,36 @@ package com.hd.screencapture.capture; +import android.annotation.TargetApi; +import android.os.Build; +import android.support.annotation.NonNull; + +import com.hd.screencapture.callback.RecorderCallback; +import com.hd.screencapture.config.ScreenCaptureConfig; +import com.hd.screencapture.observer.CaptureObserver; + /** * Created by hd on 2018/5/20 . */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) public abstract class Recorder { + + CaptureObserver observer; + + ScreenCaptureConfig config; + + RecorderCallback callback; + + Recorder(@NonNull CaptureObserver observer,@NonNull ScreenCaptureConfig config,// + @NonNull RecorderCallback callback) { + this.observer = observer; + this.config = config; + this.callback=callback; + } + + public abstract boolean prepare(); + + public abstract boolean record(); + + public abstract void release(); + } diff --git a/screencapture/src/main/java/com/hd/screencapture/capture/VideoRecorder.java b/screencapture/src/main/java/com/hd/screencapture/capture/VideoRecorder.java index 10130ca..8a0fd0b 100644 --- a/screencapture/src/main/java/com/hd/screencapture/capture/VideoRecorder.java +++ b/screencapture/src/main/java/com/hd/screencapture/capture/VideoRecorder.java @@ -1,8 +1,142 @@ package com.hd.screencapture.capture; +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.Log; +import android.view.Surface; + +import com.hd.screencapture.callback.RecorderCallback; +import com.hd.screencapture.config.ScreenCaptureConfig; +import com.hd.screencapture.observer.CaptureObserver; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Objects; + +import static android.content.ContentValues.TAG; + /** * Created by hd on 2018/5/20 . */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class VideoRecorder extends Recorder { + private Surface surface; + + private MediaCodec mEncoder; + + public VideoRecorder(@NonNull CaptureObserver observer, @NonNull ScreenCaptureConfig config,// + @NonNull RecorderCallback callback) { + super(observer, config, callback); + } + + @Override + public boolean prepare() { + try { + MediaFormat format = createMediaFormat(); + String mimeType = format.getString(MediaFormat.KEY_MIME); + mEncoder = createEncoder(mimeType); + mEncoder.setCallback(mCodecCallback); + mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + surface = mEncoder.createInputSurface(); + return true; + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + @Override + public boolean record() { + mEncoder.start(); + return true; + } + + @Override + public void release() { + if (surface != null) { + surface.release(); + surface = null; + } + if (mEncoder != null) { + mEncoder.stop(); + mEncoder.reset(); + mEncoder.release(); + mEncoder = null; + } + } + + public Surface getSurface() { + return surface; + } + + private MediaFormat createMediaFormat() { + final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC; + MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, config.getVideoConfig().getWidth(), config.getVideoConfig().getHeight()); + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + format.setInteger(MediaFormat.KEY_BIT_RATE, config.getVideoConfig().getBitrate()); + format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoConfig().getFrameRate()); + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, config.getVideoConfig().getIFrameInterval()); + if(config.allowLog()) + Log.d(TAG, "created video format: " + format); + return format; + } + + private MediaCodec createEncoder(String mimeType) throws IOException { + // try { + // // use codec name first + // if (this.mCodecName != null) { + // return MediaCodec.createByCodecName(mCodecName); + // } + // } catch (IOException e) { + // Log.w("@@", "Create MediaCodec by name '" + mCodecName + "' failure!", e); + // } + return MediaCodec.createEncoderByType(mimeType); + } + + private MediaCodec.Callback mCodecCallback = new MediaCodec.Callback() { + @Override + public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) { + callback.onInputBufferAvailable(index); + } + + @Override + public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) { + callback.onOutputBufferAvailable(index, info); + } + + @Override + public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) { + callback.onError(e); + } + + @Override + public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) { + callback.onOutputFormatChanged(format); + } + }; + + public final ByteBuffer getOutputBuffer(int index) { + return getEncoder().getOutputBuffer(index); + } + + public final ByteBuffer getInputBuffer(int index) { + return getEncoder().getInputBuffer(index); + } + + public final void queueInputBuffer(int index, int offset, int size, long pstTs, int flags) { + getEncoder().queueInputBuffer(index, offset, size, pstTs, flags); + } + + public final void releaseOutputBuffer(int index) { + getEncoder().releaseOutputBuffer(index, false); + } + + public final MediaCodec getEncoder() { + return Objects.requireNonNull(mEncoder, "doesn't prepare()"); + } } diff --git a/screencapture/src/main/java/com/hd/screencapture/config/ScreenCaptureConfig.java b/screencapture/src/main/java/com/hd/screencapture/config/ScreenCaptureConfig.java index 4f7a4b9..dc811d9 100644 --- a/screencapture/src/main/java/com/hd/screencapture/config/ScreenCaptureConfig.java +++ b/screencapture/src/main/java/com/hd/screencapture/config/ScreenCaptureConfig.java @@ -13,6 +13,11 @@ */ public class ScreenCaptureConfig extends CaptureConfig { + /** + * about logcat + */ + private boolean allowLog; + /** * about video config */ @@ -57,6 +62,14 @@ public static ScreenCaptureConfig initDefaultConfig(@NonNull Activity activity) return initDefaultConfig(VideoConfig.initDefaultConfig(activity)); } + public boolean allowLog() { + return allowLog; + } + + public void setAllowLog(boolean allowLog) { + this.allowLog = allowLog; + } + public VideoConfig getVideoConfig() { return videoConfig; } @@ -109,6 +122,11 @@ public Builder() { this.captureConfig = new ScreenCaptureConfig(); } + public Builder setAllowLog(boolean allowLog) { + captureConfig.setAllowLog(allowLog); + return this; + } + public Builder setVideoConfig(VideoConfig videoConfig) { captureConfig.setVideoConfig(videoConfig); return this; diff --git a/screencapture/src/main/java/com/hd/screencapture/config/VideoConfig.java b/screencapture/src/main/java/com/hd/screencapture/config/VideoConfig.java index 9ddc851..4136ae7 100644 --- a/screencapture/src/main/java/com/hd/screencapture/config/VideoConfig.java +++ b/screencapture/src/main/java/com/hd/screencapture/config/VideoConfig.java @@ -34,6 +34,8 @@ public class VideoConfig extends CaptureConfig { */ private int iFrameInterval = 10; + private String profileLevels ; + public static VideoConfig initDefaultConfig() { return new VideoConfig(); } diff --git a/screencapture/src/main/java/com/hd/screencapture/help/ExecutorUtil.java b/screencapture/src/main/java/com/hd/screencapture/help/ExecutorUtil.java new file mode 100644 index 0000000..56a39be --- /dev/null +++ b/screencapture/src/main/java/com/hd/screencapture/help/ExecutorUtil.java @@ -0,0 +1,51 @@ +package com.hd.screencapture.help; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * Created by hd on 2018/5/21 . + */ +public class ExecutorUtil { + + private final Executor diskIO; + + private final Executor networkIO; + + private final Executor mainThread; + + public ExecutorUtil(Executor diskIO, Executor networkIO, Executor mainThread) { + this.diskIO = diskIO; + this.networkIO = networkIO; + this.mainThread = mainThread; + } + + public ExecutorUtil() { + this(Executors.newSingleThreadExecutor(), // + Executors.newFixedThreadPool(3), new ExecutorUtil.MainThreadExecutor()); + } + + public Executor diskIO() { + return diskIO; + } + + public Executor networkIO() { + return networkIO; + } + + public Executor mainThread() { + return mainThread; + } + + private static class MainThreadExecutor implements Executor { + private Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + @Override + public void execute(@NonNull Runnable command) { + mainThreadHandler.post(command); + } + } +} diff --git a/screencapture/src/main/java/com/hd/screencapture/help/ScreenCaptureFragment.java b/screencapture/src/main/java/com/hd/screencapture/help/ScreenCaptureFragment.java index f56f6f9..14e644c 100644 --- a/screencapture/src/main/java/com/hd/screencapture/help/ScreenCaptureFragment.java +++ b/screencapture/src/main/java/com/hd/screencapture/help/ScreenCaptureFragment.java @@ -70,19 +70,23 @@ public void startCapture() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { requestPermission(); } else { - Log.e(TAG, "fuck,how can this happened !"); + if (config.allowLog()) + Log.e(TAG, "fuck,how can this happened !"); + observer.notAllowEnterNextStep(); } } } else { - Log.e(TAG, "current activity is not alive state !!!"); - stopCapture(); + if (config.allowLog()) + Log.e(TAG, "current activity is not alive state !!!"); + observer.notAllowEnterNextStep(); } } public void stopCapture() { cancelRecorder(); Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)// - .addCategory(Intent.CATEGORY_DEFAULT).setData(Uri.fromFile(config.getFile())); + .addCategory(Intent.CATEGORY_DEFAULT)// + .setData(Uri.fromFile(config.getFile())); appContext.sendBroadcast(intent); } @@ -105,8 +109,9 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis if (granted == PackageManager.PERMISSION_GRANTED) { startCapture(); } else { - Log.e(TAG, "No Permission!"); - observer.reportState(ScreenCaptureState.FAILED); + if (config.allowLog()) + Log.e(TAG, "No Permission!"); + observer.notAllowEnterNextStep(); } } } @@ -120,11 +125,14 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { if (hasPermissions()) { startRecorder(); } else { - cancelRecorder(); + if (config.allowLog()) + Log.e(TAG, "No Permission!"); + observer.notAllowEnterNextStep(); } } else { - Log.e(TAG, "media projection is null"); - observer.reportState(ScreenCaptureState.FAILED); + if (config.allowLog()) + Log.e(TAG, "media projection is null"); + observer.notAllowEnterNextStep(); } } } @@ -138,8 +146,9 @@ private void startRecorder() { if (config.isAutoMoveTaskToBack()) getActivity().moveTaskToBack(true); } else { - Log.e(TAG, "start recorder failed ,current activity is not alive state !!!"); - stopCapture(); + if (config.allowLog()) + Log.e(TAG, "start recorder failed ,current activity is not alive state !!!"); + observer.notAllowEnterNextStep(); } } @@ -164,7 +173,8 @@ private void requestPermission() { .setCancelable(false)// .setPositiveButton(android.R.string.ok, (dialog, which) -> // requestPermissions(permissions, PERMISSIONS_REQUEST_CODE))// - .setNegativeButton(android.R.string.cancel, null)// + .setNegativeButton(android.R.string.cancel, (dialog, which) -> // + observer.notAllowEnterNextStep())// .create()// .show(); } @@ -176,5 +186,4 @@ private boolean hasPermissions() { PackageManager.PERMISSION_GRANTED) | pm.checkPermission(WRITE_EXTERNAL_STORAGE, packageName); return granted == PackageManager.PERMISSION_GRANTED; } - } diff --git a/screencapture/src/main/java/com/hd/screencapture/help/ScreenCaptureRecorder.java b/screencapture/src/main/java/com/hd/screencapture/help/ScreenCaptureRecorder.java index 32f6d62..9835dda 100644 --- a/screencapture/src/main/java/com/hd/screencapture/help/ScreenCaptureRecorder.java +++ b/screencapture/src/main/java/com/hd/screencapture/help/ScreenCaptureRecorder.java @@ -4,7 +4,6 @@ import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.media.MediaCodec; -import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.media.MediaMuxer; import android.media.projection.MediaProjection; @@ -12,13 +11,16 @@ import android.os.SystemClock; import android.support.annotation.NonNull; import android.util.Log; -import android.view.Surface; +import com.hd.screencapture.callback.RecorderCallback; +import com.hd.screencapture.capture.AudioRecorder; +import com.hd.screencapture.capture.VideoRecorder; import com.hd.screencapture.config.ScreenCaptureConfig; import com.hd.screencapture.observer.CaptureObserver; -import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.LinkedList; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -39,17 +41,13 @@ public class ScreenCaptureRecorder extends Thread { private boolean mMuxerStarted = false; - private int mVideoTrackIndex = -1; - private MediaMuxer mMuxer; - private MediaCodec mEncoder; - private VirtualDisplay mVirtualDisplay; - private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo(); + private VideoRecorder videoRecorder; - private Surface mSurface; + private AudioRecorder audioRecorder; ScreenCaptureRecorder(@NonNull MediaProjection mediaProjection, @NonNull ScreenCaptureConfig config) { this.mediaProjection = mediaProjection; @@ -67,103 +65,294 @@ public void startCapture() { public void stopCapture() { recorder.set(false); + release(); } @Override public void run() { super.run(); try { - observer.reportState(ScreenCaptureState.CAPTURING); - prepareEncoder(); - mMuxer = new MediaMuxer(config.getFile().getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); - mVirtualDisplay = mediaProjection.createVirtualDisplay(TAG + "-display",// - config.getVideoConfig().getWidth(), config.getVideoConfig().getHeight(), config.getVideoConfig().getDpi(), // - DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,// - mSurface, null, null); - Log.d(TAG, "created virtual display: " + mVirtualDisplay); - recordVirtualDisplay(); + if (prepareEncoder() && startEncoder()) { + observer.reportState(ScreenCaptureState.CAPTURING); + mMuxer = new MediaMuxer(config.getFile().getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + mVirtualDisplay = mediaProjection.createVirtualDisplay(TAG + "-display",// + config.getVideoConfig().getWidth(), config.getVideoConfig().getHeight(), config.getVideoConfig().getDpi(), // + DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,// + videoRecorder.getSurface(), null, null); + } else { + throw new RuntimeException("prepare encoder failed"); + } } catch (Exception e) { e.printStackTrace(); - observer.reportState(ScreenCaptureState.FAILED); - observer.stopCapture(); - } finally { - release(); - } - } - - private void prepareEncoder() throws IOException { - final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC; // H.264 Advanced Video Coding - MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, config.getVideoConfig().getWidth(), config.getVideoConfig().getHeight()); - format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); - format.setInteger(MediaFormat.KEY_BIT_RATE, config.getVideoConfig().getBitrate()); - format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoConfig().getFrameRate()); - format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, config.getVideoConfig().getIFrameInterval()); - Log.d(TAG, "created video format: " + format); - mEncoder = MediaCodec.createEncoderByType(MIME_TYPE); - mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); - mSurface = mEncoder.createInputSurface(); - Log.d(TAG, "created input surface: " + mSurface); - mEncoder.start(); - } - - private void recordVirtualDisplay() { - final long timeoutUs = 10000; - while (recorder.get() && observer.isAlive()) { - int index = mEncoder.dequeueOutputBuffer(mBufferInfo, timeoutUs); - Log.i(TAG, "dequeue output buffer index=" + index + "=" + recorder.get()); - if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - resetOutputFormat(); - } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) { - Log.d(TAG, "retrieving buffers time out!"); + observer.notAllowEnterNextStep(); + } + } + + private boolean prepareEncoder() { + return config.hasAudio() ? (prepareVideoEncoder() && prepareAudioEncoder()) : prepareVideoEncoder(); + } + + private boolean startEncoder() { + return config.hasAudio() ? (videoRecorder.record() && audioRecorder.record()) : videoRecorder.record(); + } + + private boolean prepareVideoEncoder() { + videoRecorder = new VideoRecorder(observer, config, new RecorderCallback() { + @Override + public void onInputBufferAvailable(int index) { + if (config.allowLog()) + Log.d(TAG, "VideoRecorder onInputBufferAvailable :" + index); + } + + @Override + public void onOutputFormatChanged(MediaFormat format) { + if (config.allowLog()) + Log.d(TAG, "VideoRecorder onOutputFormatChanged:" + format); + resetVideoOutputFormat(format); + startMuxerIfReady(); + } + + @Override + public void onOutputBufferAvailable(int index, MediaCodec.BufferInfo info) { + if (config.allowLog()) + Log.i(TAG, "VideoRecorder onOutputBufferAvailable :" + index + "==" + info.size); try { - Thread.sleep(50); - } catch (InterruptedException e) { + muxVideo(index, info); + } catch (Exception e) { e.printStackTrace(); + if (config.allowLog()) + Log.e(TAG, "Muxer encountered an error! ", e); } - } else if (index >= 0) { - if (!mMuxerStarted) { - throw new IllegalStateException("MediaMuxer dose not call addTrack(format) "); + } + + @Override + public void onError(Exception exception) { + if (config.allowLog()) + Log.d(TAG, "VideoRecorder onError :" + exception); + observer.notAllowEnterNextStep(); + } + }); + return videoRecorder.prepare(); + } + + private boolean prepareAudioEncoder() { + audioRecorder = new AudioRecorder(observer, config, new RecorderCallback() { + @Override + public void onInputBufferAvailable(int index) { + if (config.allowLog()) + Log.d(TAG, "AudioRecorder onInputBufferAvailable :" + index); + } + + @Override + public void onOutputFormatChanged(MediaFormat format) { + if (config.allowLog()) + Log.d(TAG, "AudioRecorder onOutputFormatChanged :" + format); + resetAudioOutputFormat(format); + startMuxerIfReady(); + } + + @Override + public void onOutputBufferAvailable(int index, MediaCodec.BufferInfo info) { + if (config.allowLog()) + Log.i(TAG, "AudioRecorder onOutputBufferAvailable: " + index + "==" + info.size); + try { + muxAudio(index, info); + } catch (Exception e) { + if (config.allowLog()) + Log.e(TAG, "Muxer encountered an error! ", e); } - encodeToVideoTrack(index); - mEncoder.releaseOutputBuffer(index, false); } + + @Override + public void onError(Exception exception) { + if (config.allowLog()) + Log.d(TAG, "AudioRecorder onError :" + "==" + exception); + observer.notAllowEnterNextStep(); + } + }); + return audioRecorder.prepare(); + } + + private final int INVALID_INDEX = -1; + + private byte[] sps, pps; + + private MediaFormat mVideoOutputFormat = null, mAudioOutputFormat = null; + + private int mVideoTrackIndex = INVALID_INDEX, mAudioTrackIndex = INVALID_INDEX; + + private LinkedList mPendingVideoEncoderBufferIndices = new LinkedList<>(); + + private LinkedList mPendingAudioEncoderBufferIndices = new LinkedList<>(); + + private LinkedList mPendingAudioEncoderBufferInfos = new LinkedList<>(); + + private LinkedList mPendingVideoEncoderBufferInfos = new LinkedList<>(); + + private void resetVideoOutputFormat(MediaFormat newFormat) { + if (mVideoTrackIndex >= 0 || mMuxerStarted) { + throw new IllegalStateException("output format already changed!"); + } + mVideoOutputFormat = newFormat; + sps = newFormat.getByteBuffer("csd-0").array(); + pps = newFormat.getByteBuffer("csd-1").array(); + if (config.allowLog()) + Log.i(TAG, "Video output format changed.\n New format: " + newFormat.toString() +// + "\nvideo sps :" + Arrays.toString(sps) + "\nvideo pps :" + Arrays.toString(pps)); + // video sps :[0, 0, 0, 1, 103, 66, -128, 42, -38, 1, 16, 15, 30, 94, 82, 10, 12, 12, 13, -95, 66, 106] + // video pps :[0, 0, 0, 1, 104, -50, 6, -30] + observer.reportVideoHeaderByte(sps, pps); + } + + private void resetAudioOutputFormat(MediaFormat newFormat) { + if (mAudioTrackIndex >= 0 || mMuxerStarted) { + throw new IllegalStateException("output format already changed!"); } - observer.reportState(ScreenCaptureState.COMPLETED); + mAudioOutputFormat = newFormat; + if (config.allowLog()) + Log.i(TAG, "Audio output format changed.\n New format: " + newFormat.toString()); } - private void encodeToVideoTrack(int index) { - ByteBuffer encodedData = mEncoder.getOutputBuffer(index); - if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + private void startMuxerIfReady() { + if (mMuxerStarted || mVideoOutputFormat == null || (audioRecorder != null && mAudioOutputFormat == null)) { + return; + } + mVideoTrackIndex = mMuxer.addTrack(mVideoOutputFormat); + mAudioTrackIndex = !config.hasAudio() && audioRecorder == null ? INVALID_INDEX : mMuxer.addTrack(mAudioOutputFormat); + mMuxer.start(); + mMuxerStarted = true; + if (config.allowLog()) + Log.i(TAG, "Started media muxer, videoIndex=" + mVideoTrackIndex); + if (mPendingVideoEncoderBufferIndices.isEmpty() && mPendingAudioEncoderBufferIndices.isEmpty()) { + return; + } + if (config.allowLog()) + Log.i(TAG, "Mux pending video output buffers..."); + MediaCodec.BufferInfo info; + while ((info = mPendingVideoEncoderBufferInfos.poll()) != null) { + int index = mPendingVideoEncoderBufferIndices.poll(); + muxVideo(index, info); + } + if (config.hasAudio() && audioRecorder != null) { + while ((info = mPendingAudioEncoderBufferInfos.poll()) != null) { + int index = mPendingAudioEncoderBufferIndices.poll(); + muxAudio(index, info); + } + } + if (config.allowLog()) + Log.i(TAG, "Mux pending video output buffers done."); + } + + private void muxVideo(int index, MediaCodec.BufferInfo info) { + if (!recorder.get()) { + if (config.allowLog()) + Log.w(TAG, "muxVideo: Already stopped!"); + return; + } + if (!mMuxerStarted || mVideoTrackIndex == INVALID_INDEX) { + mPendingVideoEncoderBufferIndices.add(index); + mPendingVideoEncoderBufferInfos.add(info); + return; + } + ByteBuffer encodedData = videoRecorder.getOutputBuffer(index); + writeSampleData(mVideoTrackIndex, info, encodedData); + videoRecorder.releaseOutputBuffer(index); + if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + if (config.allowLog()) + Log.d(TAG, "Stop encoder and muxer, since the buffer has been marked with EOS"); + mVideoTrackIndex = INVALID_INDEX; + } + } + + private void muxAudio(int index, MediaCodec.BufferInfo info) { + if (!recorder.get()) { + if (config.allowLog()) + Log.w(TAG, "muxAudio: Already stopped!"); + return; + } + if (!mMuxerStarted || mAudioTrackIndex == INVALID_INDEX) { + mPendingAudioEncoderBufferIndices.add(index); + mPendingAudioEncoderBufferInfos.add(info); + return; + } + ByteBuffer encodedData = audioRecorder.getOutputBuffer(index); + writeSampleData(mAudioTrackIndex, info, encodedData); + audioRecorder.releaseOutputBuffer(index); + if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + if (config.allowLog()) + Log.d(TAG, "Stop encoder and muxer, since the buffer has been marked with EOS"); + mAudioTrackIndex = INVALID_INDEX; + } + } + + private void writeSampleData(int track, MediaCodec.BufferInfo info, ByteBuffer encodedData) { + if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { // The codec config data was pulled out and fed to the muxer when we got // the INFO_OUTPUT_FORMAT_CHANGED status. // Ignore it. - Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG"); - mBufferInfo.size = 0; + if (config.allowLog()) + Log.d(TAG, "Ignoring BUFFER_FLAG_CODEC_CONFIG"); + info.size = 0; } - if (mBufferInfo.size == 0) { + boolean eos = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; + if (info.size == 0 && !eos) { Log.d(TAG, "info.size == 0, drop it."); encodedData = null; } else { - Log.d(TAG, "got buffer, info: size=" + mBufferInfo.size +// - ", presentationTimeUs=" + mBufferInfo.presentationTimeUs + ", offset=" + mBufferInfo.offset); + if (info.presentationTimeUs != 0) { // maybe 0 if eos + if (track == mVideoTrackIndex) { + setCaptureTime(true, info); + } else if (track == mAudioTrackIndex) { + setCaptureTime(false, info); + } + } + if (config.allowLog()) + Log.d(TAG, "Got buffer, track=" + track + ", info: size=" + info.size + ", presentationTimeUs=" + info.presentationTimeUs); } if (encodedData != null) { - setCaptureTime(); - encodedData.position(mBufferInfo.offset); - encodedData.limit(mBufferInfo.offset + mBufferInfo.size); - mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo); - Log.i(TAG, "sent " + mBufferInfo.size + " bytes to muxer..."); + encodedData.position(info.offset); + encodedData.limit(info.offset + info.size); + mMuxer.writeSampleData(track, encodedData, info); + if (config.allowLog()) + Log.i(TAG, "Sent " + info.size + " bytes to MediaMuxer on track " + track); + reportData(track, info, encodedData); } } - private void setCaptureTime() { - if (mBufferInfo.presentationTimeUs != 0) { - resetVideoPts(mBufferInfo); + private void reportData(int track, MediaCodec.BufferInfo info, ByteBuffer encodedData) { + byte[] bytes = null; + if (track == mVideoTrackIndex) {//send video data + if (info.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) { + bytes = new byte[info.size + sps.length + pps.length]; + System.arraycopy(sps, 0, bytes, 0, sps.length); + System.arraycopy(pps, 0, bytes, sps.length, pps.length); + encodedData.get(bytes, sps.length + pps.length, info.size); + } else { + bytes = new byte[info.size]; + encodedData.get(bytes, 0, info.size); + } + observer.reportVideoContentByte(bytes); + } else if (track == mAudioTrackIndex) {//send audio data + bytes = new byte[info.size]; + encodedData.get(bytes, 0, info.size); + observer.reportAudioContentByte(bytes); + } + if (config.allowLog() && bytes != null) + Log.i(TAG, "report video data :" + bytes.length + "==" + Arrays.toString(bytes)); + } + + private void setCaptureTime(boolean isVideo, MediaCodec.BufferInfo info) { + if (info.presentationTimeUs != 0) { + if (isVideo) { + resetVideoPts(info); + } else { + resetAudioPts(info); + } } if (startTime <= 0) { - startTime = mBufferInfo.presentationTimeUs; + startTime = info.presentationTimeUs; } - long time = (mBufferInfo.presentationTimeUs - startTime) / 1000 / 1000; + long time = (info.presentationTimeUs - startTime) / 1000 / 1000; //no need to report when time less than one second if (SystemClock.elapsedRealtime() - mLastFiredTime < 1000) { return; @@ -172,7 +361,16 @@ private void setCaptureTime() { mLastFiredTime = SystemClock.elapsedRealtime(); } - private long mVideoPtsOffset, startTime, mLastFiredTime; + private long mVideoPtsOffset, mAudioPtsOffset, startTime, mLastFiredTime; + + private void resetAudioPts(MediaCodec.BufferInfo buffer) { + if (mAudioPtsOffset == 0) { + mAudioPtsOffset = buffer.presentationTimeUs; + buffer.presentationTimeUs = 0; + } else { + buffer.presentationTimeUs -= mAudioPtsOffset; + } + } private void resetVideoPts(MediaCodec.BufferInfo buffer) { if (mVideoPtsOffset == 0) { @@ -183,26 +381,12 @@ private void resetVideoPts(MediaCodec.BufferInfo buffer) { } } - private void resetOutputFormat() { - // should happen before receiving buffers, and should only happen once - if (mMuxerStarted) { - throw new IllegalStateException("output format already changed!"); - } - MediaFormat newFormat = mEncoder.getOutputFormat(); - Log.i(TAG, "output format changed.\n new format: " + newFormat.toString()); - mVideoTrackIndex = mMuxer.addTrack(newFormat); - mMuxer.start(); - mMuxerStarted = true; - Log.i(TAG, "started media muxer, videoIndex=" + mVideoTrackIndex); - } - private void release() { - if (mEncoder != null) { - mEncoder.stop(); - mEncoder.reset(); - mEncoder.release(); - mEncoder = null; - } + stopEncoders(); + signalStop(); + mVideoOutputFormat = mAudioOutputFormat = null; + mVideoTrackIndex = mAudioTrackIndex = INVALID_INDEX; + mMuxerStarted = false; if (mVirtualDisplay != null) { mVirtualDisplay.release(); mVirtualDisplay = null; @@ -212,10 +396,48 @@ private void release() { mediaProjection = null; } if (mMuxer != null) { - mMuxer.stop(); - mMuxer.release(); + try { + mMuxer.stop(); + mMuxer.release(); + } catch (Exception e) { + // ignored + } mMuxer = null; } - Log.d("tag", "recorder release complete"); + if (config.allowLog()) + Log.d(TAG, "recorder release complete"); + } + + private void stopEncoders() { + mPendingAudioEncoderBufferInfos.clear(); + mPendingAudioEncoderBufferIndices.clear(); + mPendingVideoEncoderBufferInfos.clear(); + mPendingVideoEncoderBufferIndices.clear(); + // maybe called on an error has been occurred + if (videoRecorder != null) { + videoRecorder.release(); + videoRecorder = null; + } + if (audioRecorder != null) { + audioRecorder.release(); + audioRecorder = null; + } } + + private void signalStop() { + MediaCodec.BufferInfo eos = new MediaCodec.BufferInfo(); + ByteBuffer buffer = ByteBuffer.allocate(0); + eos.set(0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + if (config.allowLog()) + Log.i(TAG, "Signal EOS to muxer "); + if (mVideoTrackIndex != INVALID_INDEX) { + writeSampleData(mVideoTrackIndex, eos, buffer); + } + if (mAudioTrackIndex != INVALID_INDEX) { + writeSampleData(mAudioTrackIndex, eos, buffer); + } + mVideoTrackIndex = INVALID_INDEX; + mAudioTrackIndex = INVALID_INDEX; + } + } diff --git a/screencapture/src/main/java/com/hd/screencapture/observer/CaptureObserver.java b/screencapture/src/main/java/com/hd/screencapture/observer/CaptureObserver.java index b676a7b..c14d310 100644 --- a/screencapture/src/main/java/com/hd/screencapture/observer/CaptureObserver.java +++ b/screencapture/src/main/java/com/hd/screencapture/observer/CaptureObserver.java @@ -2,8 +2,9 @@ import com.hd.screencapture.ScreenCapture; import com.hd.screencapture.callback.ScreenCaptureCallback; -import com.hd.screencapture.help.ScreenCaptureState; +import com.hd.screencapture.callback.ScreenCaptureStreamCallback; import com.hd.screencapture.config.ScreenCaptureConfig; +import com.hd.screencapture.help.ScreenCaptureState; /** * Created by hd on 2018/5/15 . @@ -30,7 +31,12 @@ public boolean isAlive() { } public void stopCapture() { - screenCapture.stopCapture(); + if (screenCapture.isRunning()) screenCapture.stopCapture(); + } + + public void notAllowEnterNextStep() { + reportState(ScreenCaptureState.FAILED); + stopCapture(); } public void reportState(ScreenCaptureState state) { @@ -50,4 +56,32 @@ public void reportTime(long time) { } } } + + public void reportVideoHeaderByte(byte[] sps, byte[] pps) { + if (isAlive() && config != null && sps != null && sps.length > 0 && pps != null && pps.length > 0) { + ScreenCaptureCallback callback = config.getCaptureCallback(); + if (callback != null && callback instanceof ScreenCaptureStreamCallback) { + ((ScreenCaptureStreamCallback) callback).videoHeaderByte(sps, pps); + } + } + } + + public void reportVideoContentByte(byte[] content) { + if (isAlive() && config != null && content != null && content.length > 0) { + ScreenCaptureCallback callback = config.getCaptureCallback(); + if (callback != null && callback instanceof ScreenCaptureStreamCallback) { + ((ScreenCaptureStreamCallback) callback).videoContentByte(content); + } + } + } + + public void reportAudioContentByte(byte[] content) { + if (isAlive() && config != null && content != null && content.length > 0) { + ScreenCaptureCallback callback = config.getCaptureCallback(); + if (callback != null && callback instanceof ScreenCaptureStreamCallback) { + ((ScreenCaptureStreamCallback) callback).audioContentByte(content); + } + } + } + } diff --git a/screencapture/src/main/java/com/hd/screencapture/observer/ScreenCaptureObserver.java b/screencapture/src/main/java/com/hd/screencapture/observer/ScreenCaptureObserver.java index 0f4e768..1c58ea4 100644 --- a/screencapture/src/main/java/com/hd/screencapture/observer/ScreenCaptureObserver.java +++ b/screencapture/src/main/java/com/hd/screencapture/observer/ScreenCaptureObserver.java @@ -29,5 +29,6 @@ private void onResume() { private void onDestroy() { Log.d(TAG, "onDestroy"); alive = false; + stopCapture(); } }