diff --git a/README.md b/README.md index f303afd..e0cf042 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ +2018.5.15 +1. 使用 ImageReader 的 Surface 作为录屏输入,然后将输入数据通过 FFmpegRTMPRecorder 发送出去,但是不成功,只有声音,没有画面。 +2. 使用 MediaCodec 的Surface 作为录屏输入,此输入数据已经是 h264 压缩格式,无法传送给 FFmpegRTMPRecorder,没有测试成功; +3. 使用 FFmpegRTMPRecorder 录屏推流没有成功,成功的例子是使用腾讯直播SDK,见我的另一个例子:https://github.com/smartsharp/TestTencentRtmp。 +4. 使用心得: https://www.jianshu.com/p/2e21d81b350c + + + + + + + + + + # RtmpRecoder Record camera and push stream to rtmp server. diff --git a/app/build.gradle b/app/build.gradle index 9c3e808..7ef21f6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,17 +1,21 @@ apply plugin: 'com.android.application' // This does not break the build when Android Studio is missing the JRebel for Android plugin. -apply plugin: 'com.zeroturnaround.jrebel.android' +//apply plugin: 'com.zeroturnaround.jrebel.android' android { - compileSdkVersion 23 - buildToolsVersion "23.0.2" + compileSdkVersion 26 defaultConfig { applicationId "cn.campusapp.rtmprecorder" - minSdkVersion 14 - targetSdkVersion 23 + minSdkVersion 23 + targetSdkVersion 26 versionCode 1 versionName "1.0" + + ndk { + //abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a', 'arm64-v8a' + abiFilters 'arm64-v8a' + } } buildTypes { debug { @@ -31,14 +35,20 @@ android { } dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - testCompile 'junit:junit:4.12' - compile 'com.android.support:appcompat-v7:23.1.1' - compile 'com.android.support:design:23.1.1' - compile 'com.jakewharton:butterknife:7.0.1' - compile group: 'org.bytedeco', name: 'javacv', version: '1.1' - compile group: 'org.bytedeco.javacpp-presets', name: 'opencv', version: '3.0.0-1.1', classifier: 'android-arm' - compile group: 'org.bytedeco.javacpp-presets', name: 'opencv', version: '3.0.0-1.1', classifier: 'android-x86' - compile group: 'org.bytedeco.javacpp-presets', name: 'ffmpeg', version: '2.8.1-1.1', classifier: 'android-arm' - compile group: 'org.bytedeco.javacpp-presets', name: 'ffmpeg', version: '2.8.1-1.1', classifier: 'android-x86' + implementation fileTree(include: ['*.jar'], dir: 'libs') + testImplementation 'junit:junit:4.12' + implementation 'com.android.support:appcompat-v7:26.1.0' + implementation 'com.android.support:design:26.1.0' + //implementation 'com.jakewharton:butterknife:8.8.1' + //implementation 'org.bytedeco:javacv:+' + //implementation 'org.bytedeco.javacpp-presets:opencv:3.0.0-1.1:android-x86' + //implementation 'org.bytedeco.javacpp-presets:ffmpeg:2.8.1-1.1:android-x86' + //implementation 'org.bytedeco.javacpp-presets:opencv:3.0.0-1.1:android-arm' + //implementation 'org.bytedeco.javacpp-presets:ffmpeg:2.8.1-1.1:android-arm' + implementation 'org.bytedeco:javacv:1.4.1' + //implementation group: 'org.bytedeco.javacpp-presets', name: 'opencv-platform', version: '3.4.1-1.4.1' + //implementation group: 'org.bytedeco.javacpp-presets', name: 'ffmpeg-platform', version: '3.4.2-1.4.1' + implementation 'org.bytedeco.javacpp-presets:opencv:3.4.1-1.4.1:android-arm64' + implementation 'org.bytedeco.javacpp-presets:ffmpeg:3.4.2-1.4.1:android-arm64' + implementation project(':libyuv') } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a157ca7..496f920 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,12 +2,13 @@ - - - - - + + + + + + + android:theme="@style/AppTheme"> @@ -27,8 +28,15 @@ + android:label="@string/title_activity_record"/> + + + diff --git a/app/src/main/java/cn/campusapp/rtmprecorder/ImageUtil.java b/app/src/main/java/cn/campusapp/rtmprecorder/ImageUtil.java new file mode 100644 index 0000000..7b306fd --- /dev/null +++ b/app/src/main/java/cn/campusapp/rtmprecorder/ImageUtil.java @@ -0,0 +1,55 @@ +package cn.campusapp.rtmprecorder; + +public class ImageUtil { + + /** + * Converts YUV420 NV21 to ARGB8888 + * + * @param data byte array on YUV420 NV21 format. + * @param width pixels width + * @param height pixels height + * @return a ARGB8888 pixels int array. Where each int is a pixels ARGB. + */ + public static int[] convertYUV420_NV21toARGB8888(byte [] data, int width, int height) { + int size = width*height; + int offset = size; + int[] pixels = new int[size]; + int u, v, y1, y2, y3, y4; + + // i along Y and the final pixels + // k along pixels U and V + for(int i=0, k=0; i < size; i+=2, k+=2) { + y1 = data[i ]&0xff; + y2 = data[i+1]&0xff; + y3 = data[width+i ]&0xff; + y4 = data[width+i+1]&0xff; + + u = data[offset+k ]&0xff; + v = data[offset+k+1]&0xff; + u = u-128; + v = v-128; + + pixels[i ] = convertYUVtoARGB(y1, u, v); + pixels[i+1] = convertYUVtoARGB(y2, u, v); + pixels[width+i ] = convertYUVtoARGB(y3, u, v); + pixels[width+i+1] = convertYUVtoARGB(y4, u, v); + + if (i!=0 && (i+2)%width==0) + i+=width; + } + + return pixels; + } + + private static int convertYUVtoARGB(int y, int u, int v) { + int r,g,b; + + r = y + (int)1.402f*u; + g = y - (int)(0.344f*v +0.714f*u); + b = y + (int)1.772f*v; + r = r>255? 255 : r<0 ? 0 : r; + g = g>255? 255 : g<0 ? 0 : g; + b = b>255? 255 : b<0 ? 0 : b; + return 0xff000000 | (r<<16) | (g<<8) | b; + } +} diff --git a/app/src/main/java/cn/campusapp/rtmprecorder/MainActivity.java b/app/src/main/java/cn/campusapp/rtmprecorder/MainActivity.java index 0d02791..a3bae85 100644 --- a/app/src/main/java/cn/campusapp/rtmprecorder/MainActivity.java +++ b/app/src/main/java/cn/campusapp/rtmprecorder/MainActivity.java @@ -2,28 +2,45 @@ import android.os.Bundle; import android.support.v7.app.AppCompatActivity; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; -import butterknife.ButterKnife; -import butterknife.OnClick; +//import butterknife.ButterKnife; +//import butterknife.OnClick; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements View.OnClickListener { + + private static final String STREAM_URL = "rtmp://192.168.30.77/live/livestream"; - private static final String STREAM_URL = "rtmp://10.10.5.119/live/livestream"; - @OnClick(R.id.record_btn) - void onRecordClick(){ - startActivity(RecordActivity.makeIntent(STREAM_URL)); - } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - ButterKnife.bind(this); + //ButterKnife.bind(this); + Button button = (Button)findViewById(R.id.record_btn); + button.setOnClickListener(this); + button = (Button)findViewById(R.id.record_btn2); + button.setOnClickListener(this); } protected String getUrl(){ return STREAM_URL; } + + @Override + public void onClick(View v) { + if(PermissionUtil.isGrantPermissionsRequired(this)) { + if(v.getId()==R.id.record_btn) { + startActivity(RecordActivity.makeIntent(STREAM_URL)); + }else { + startActivity(RecordScreenActivity.makeIntent(STREAM_URL)); + } + }else { + Toast.makeText(this, "Permission requested.", Toast.LENGTH_SHORT).show(); + } + } } diff --git a/app/src/main/java/cn/campusapp/rtmprecorder/PermissionUtil.java b/app/src/main/java/cn/campusapp/rtmprecorder/PermissionUtil.java new file mode 100644 index 0000000..639dffa --- /dev/null +++ b/app/src/main/java/cn/campusapp/rtmprecorder/PermissionUtil.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014 Pierre Chabardes + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.campusapp.rtmprecorder; + +import android.Manifest; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Build; + +/** + * Created by zhanghongbiao@vargo.com.cn on 18-4-11. + */ + +public class PermissionUtil { + + public static final int PERMISSIONS_CODE = 1; + + public static boolean isGrantPermissionsRequired(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && activity.checkSelfPermission( + Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + + activity.requestPermissions(new String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.CAMERA + }, PERMISSIONS_CODE); + + return false; + } + + return true; + } +} diff --git a/app/src/main/java/cn/campusapp/rtmprecorder/RecordActivity.java b/app/src/main/java/cn/campusapp/rtmprecorder/RecordActivity.java index acf9115..6f76ade 100644 --- a/app/src/main/java/cn/campusapp/rtmprecorder/RecordActivity.java +++ b/app/src/main/java/cn/campusapp/rtmprecorder/RecordActivity.java @@ -10,6 +10,7 @@ import android.media.AudioRecord; import android.media.MediaRecorder; import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.Display; import android.view.KeyEvent; @@ -33,10 +34,11 @@ import java.util.Comparator; import java.util.List; -public class RecordActivity extends Activity implements OnClickListener { +public class RecordActivity extends AppCompatActivity implements OnClickListener { - private final static String CLASS_LABEL = "RecordActivity"; + private final static String CLASS_LABEL = "zhanghb/RecordActivity"; private final static String LOG_TAG = CLASS_LABEL; + private final static String TAG = CLASS_LABEL; /* The number of seconds in the continuous record loop (or 0 to disable loop). */ final int RECORD_LENGTH = 0; /* layout setting */ @@ -161,7 +163,7 @@ private void initLayout() { //--------------------------------------- private void initRecorder() { - Log.w(LOG_TAG, "init recorder"); + Log.d(LOG_TAG, "init recorder: "+RECORD_LENGTH); if (RECORD_LENGTH > 0) { imagesIndex = 0; @@ -198,11 +200,12 @@ public void startRecording() { try { recorder.start(); startTime = System.currentTimeMillis(); - recording = true; audioThread.start(); + recording = true; } catch (FFmpegFrameRecorder.Exception e) { e.printStackTrace(); + Log.e(TAG, "startRecording exception "+e+", "+Log.getStackTraceString(e)); } } @@ -268,6 +271,7 @@ public void stopRecording() { recorder.release(); } catch (FFmpegFrameRecorder.Exception e) { e.printStackTrace(); + Log.e(TAG, "stopRecording exception "+e+", "+Log.getStackTraceString(e)); } recorder = null; @@ -292,15 +296,16 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { @Override public void onClick(View v) { + Log.i(TAG, "onClick: "+recording); if (!recording) { startRecording(); Log.w(LOG_TAG, "Start Button Pushed"); - btnRecorderControl.setText("Stop"); + if(recording) btnRecorderControl.setText("Stop"); } else { // This will trigger the audio recording loop to stop and then set isRecorderStart = false; stopRecording(); Log.w(LOG_TAG, "Stop Button Pushed"); - btnRecorderControl.setText("Start"); + if(!recording) btnRecorderControl.setText("Start"); } } diff --git a/app/src/main/java/cn/campusapp/rtmprecorder/RecordScreenActivity.java b/app/src/main/java/cn/campusapp/rtmprecorder/RecordScreenActivity.java new file mode 100644 index 0000000..e510ead --- /dev/null +++ b/app/src/main/java/cn/campusapp/rtmprecorder/RecordScreenActivity.java @@ -0,0 +1,162 @@ +package cn.campusapp.rtmprecorder; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.ActivityInfo; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.os.Bundle; +import android.os.Environment; +import android.os.IBinder; +import android.support.v7.app.AppCompatActivity; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.Toast; + +import java.io.File; + + +public class RecordScreenActivity extends AppCompatActivity implements OnClickListener { + + private final static String CLASS_LABEL = "zhanghb/RSActivity"; + private final static String LOG_TAG = CLASS_LABEL; + private final static String TAG = CLASS_LABEL; + + private Button btnControl; + private static final String KEY_STREAM_URL = "stream_url"; + + String ffmpeg_link; + RecordScreenService mService; + MediaProjectionManager projectionManager; + + public static Intent makeIntent(String streamUrl){ + Intent intent = new Intent(App.getContext(), RecordScreenActivity.class); + intent.putExtra(KEY_STREAM_URL, streamUrl); + return intent; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ffmpeg_link = getIntent().getStringExtra(KEY_STREAM_URL); + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + + setContentView(R.layout.activity_record); + + btnControl = (Button) findViewById(R.id.recorder_control); + btnControl.setText("Start"); + btnControl.setOnClickListener(this); + btnControl.setEnabled(false); + + projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE); + Intent intent = new Intent(this, RecordScreenService.class); + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + @Override + protected void onDestroy() { + Log.d(TAG, "onDestroy"); + unbindService(mConnection); + super.onDestroy(); + } + + @Override + public void onResume() { + super.onResume(); + } + + private ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + try { + RecordScreenService.RecordScreenBinder binder = + (RecordScreenService.RecordScreenBinder) service; + mService = binder.getRecordService(); + if(mService != null){ + if(mService.isRunning()) { + btnControl.setText(R.string.stop_record); + } + btnControl.setEnabled(true); + } + }catch (Exception e){ + Log.e(TAG, "onServiceConnected exception "+e+","+Log.getStackTraceString(e)); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mService = null; + } + }; + + + @Override + public void onClick(View v) { + Log.i(TAG, "onClick: "+mService.isRunning()); + if (!mService.isRunning()) { + Intent captureIntent= projectionManager.createScreenCaptureIntent(); + startActivityForResult(captureIntent, 1); + } else { + mService.stopRecord(); + btnControl.setText(R.string.start_record); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + //super.onActivityResult(requestCode, resultCode, data); + if (requestCode == 1 && resultCode == RESULT_OK) { + DisplayMetrics mDisplayMetrics = new DisplayMetrics();//屏幕分辨率容器 + getWindowManager().getDefaultDisplay().getMetrics(mDisplayMetrics); + int width = mDisplayMetrics.widthPixels; + int height = mDisplayMetrics.heightPixels; + float density = mDisplayMetrics.density; + int densityDpi = mDisplayMetrics.densityDpi; + Log.d(TAG,"Screen Ratio: ["+width+"x"+height+"],density="+density+",densityDpi="+densityDpi); + mService.setDispParams(width, height, densityDpi); + if(ffmpeg_link != null) { + mService.setOutputUrl(ffmpeg_link); + }else { + mService.setOutputUrl(getSavedFileUrl()); + } + mService.setMediaProjection(projectionManager.getMediaProjection(resultCode, data)); + mService.startRecord(); + if (mService.isRunning()) { + btnControl.setText(R.string.stop_record); + } + } + } + + public String getSavedFileUrl() { + String filepath = getsaveDirectory() + System.currentTimeMillis() + ".mp4"; + return "file://"+filepath; + } + + public String getsaveDirectory() { + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + String rootDir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "ScreenRecord" + "/"; + + File file = new File(rootDir); + if (!file.exists()) { + if (!file.mkdirs()) { + return null; + } + } + + Toast.makeText(getApplicationContext(), rootDir, Toast.LENGTH_SHORT).show(); + + return rootDir; + } else { + return null; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/cn/campusapp/rtmprecorder/RecordScreenService.java b/app/src/main/java/cn/campusapp/rtmprecorder/RecordScreenService.java new file mode 100644 index 0000000..9441e10 --- /dev/null +++ b/app/src/main/java/cn/campusapp/rtmprecorder/RecordScreenService.java @@ -0,0 +1,601 @@ +package cn.campusapp.rtmprecorder; + +import android.app.Service; +import android.content.Intent; +import android.graphics.ImageFormat; +import android.graphics.PixelFormat; +import android.graphics.SurfaceTexture; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.Image; +import android.media.ImageReader; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.media.MediaRecorder; +import android.media.projection.MediaProjection; +import android.os.Binder; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Message; +import android.support.annotation.NonNull; +import android.util.Log; +import android.view.Surface; + +import com.wuwang.libyuv.Key; +import com.wuwang.libyuv.YuvUtils; + +import org.bytedeco.javacv.FFmpegFrameRecorder; +import org.bytedeco.javacv.Frame; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ShortBuffer; + +import static org.bytedeco.javacpp.avcodec.AV_CODEC_ID_H264; + +public class RecordScreenService extends Service { + private static final String TAG = "zhanghb/RSService"; + private HandlerThread myHandlerThread; + private Handler myHandler; + private boolean running; + private int mWidth, mHeight, mDpi; + private MediaProjection mediaProjection; + private MediaRecorder mediaRecorder; + private VirtualDisplay virtualDisplay; + private String outputUrl; + private boolean recordToFile = false; + // add for rtmp encoder + private FFmpegFrameRecorder frameRecorder; + private int frameRate = 30; + + private MediaCodec mEncoder; + private long mVideoPtsOffset, mAudioPtsOffset; + + private long startTime; + private Frame yuvImage = null; + private Surface mSurface; + private static final int MAX_IMAGE_NUMBER = 25;//30;//这个值代表ImageReader最大的存储图像 + private ImageReader mImageReader; + + private static final int FRAME_FROM_MY_SURFACE = 0; + private static final int FRAME_FROM_IMAGE_READER = 1; + private static final int FRAME_FROM_MEDIA_CODEC = 2; + private static final boolean RECORD_WITH_AUTIO = true; + + private static final int frame_source = FRAME_FROM_IMAGE_READER; + + /* audio data getting thread */ + private AudioRecord audioRecord; + private AudioRecordRunnable audioRecordRunnable; + private Thread audioThread; + boolean recording = false; + volatile boolean runAudioThread = true; + private int sampleAudioRateInHz = 44100; + + private RtmpThread mRtmpThread; + private Object frameLock = new Object(); + private boolean frameAvail = false; + + // mysurface + private int mTextureId = 1; + private SurfaceTexture mSurfaceTexture; + + public RecordScreenService() { + } + + @Override + public void onCreate() { + super.onCreate(); + myHandlerThread = new HandlerThread("service_thread", + android.os.Process.THREAD_PRIORITY_BACKGROUND); + myHandlerThread.start(); + myHandler = new Handler( myHandlerThread.getLooper() ){ + @Override + public void handleMessage(Message msg) { + //super.handleMessage(msg); + //这个方法是运行在 handler-thread 线程中的 ,可以执行耗时操作 + Log.d( "handler " , "消息: " + msg.what + " 线程: " + Thread.currentThread().getName() ) ; + } + }; + running = false; + } + + @Override + public void onDestroy() { + myHandlerThread.quit(); + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + Log.e(TAG, "onBind"); + return new RecordScreenBinder(); + } + + @Override + public boolean onUnbind(Intent intent) { + Log.e(TAG, "onUnbind"); + stopRecord(); + return super.onUnbind(intent); + } + + public void setDispParams(int width, int height, int densityDpi) { + this.mWidth = width; + this.mHeight = height; + this.mDpi = densityDpi; + } + + public void setOutputUrl(String outputUrl){ + this.outputUrl = outputUrl; + } + + public class RecordScreenBinder extends Binder { + public RecordScreenService getRecordService() { + return RecordScreenService.this; + } + } + + public boolean isRunning() { + return running; + } + + public void setMediaProjection(MediaProjection m){ + mediaProjection = m; + } + + public boolean startRecord() { + if (mediaProjection == null || outputUrl == null || running) { + return false; + } + Log.d(TAG, "startRecord "+outputUrl); + recordToFile = outputUrl.startsWith("file://"); + if(recordToFile) { + if(initFileRecorder()){ + mediaRecorder.start(); + running = true; + } + }else{ + if(initRtmpRecorder()) { + try { + if(mRtmpThread != null){ + mRtmpThread.cancel(); + } + if(frame_source != FRAME_FROM_MEDIA_CODEC){ + mRtmpThread = new RtmpThread(); + mRtmpThread.start(); + } + frameRecorder.start(); + startTime = System.currentTimeMillis(); + if(RECORD_WITH_AUTIO) { + audioThread.start(); + } + if(frame_source == FRAME_FROM_MEDIA_CODEC){ + mEncoder.start(); + } + recording = true; + running = true; + }catch (Exception e){ + Log.e(TAG, "startRecord exception "+e+","+Log.getStackTraceString(e)); + deinitRtmpRecorder(); + } + } + } + Log.d(TAG, "startRecord result="+running); + return running; + } + public boolean stopRecord() { + if (!running) { + return false; + } + Log.d(TAG, "stopRecord "+outputUrl); + if(recordToFile) { + deinitFileRecorder(); + }else{ + deinitRtmpRecorder(); + } + running = false; + + return true; + } + private boolean initFileRecorder() { + Log.d(TAG, "initFileRecorder "); + try { + mediaRecorder = new MediaRecorder(); + mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); + mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); + mediaRecorder.setOutputFile(outputUrl.substring(7)); + mediaRecorder.setVideoSize(mWidth, mHeight); + mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); + mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); + mediaRecorder.setVideoEncodingBitRate(5 * 1024 * 1024); + mediaRecorder.setVideoFrameRate(30); + mediaRecorder.prepare(); + mSurface = mediaRecorder.getSurface(); + virtualDisplay = mediaProjection.createVirtualDisplay("MainScreen", mWidth, mHeight, mDpi, + DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mSurface, null, null); + return true; + } catch (IOException e) { + e.printStackTrace(); + Log.e(TAG, "initFileRecorder exception "+e+","+Log.getStackTraceString(e)); + deinitFileRecorder(); + return false; + } + } + private void deinitFileRecorder() { + Log.d(TAG, "deinitFileRecorder "); + if(mediaRecorder != null) { + mediaRecorder.stop(); + //mediaRecorder.reset(); + mediaRecorder.release(); + if(virtualDisplay != null) + virtualDisplay.release(); + mediaProjection.stop(); + mediaRecorder = null; + } + } + private boolean initRtmpRecorder() { + Log.d(TAG, "initRtmpRecorder "+","+frame_source); + try { + if (mRtmpThread != null) { + mRtmpThread.cancel(); + mRtmpThread = null; + } + // init rtmp recorder + yuvImage = new Frame(mWidth, mHeight, Frame.DEPTH_UBYTE, 4); + + frameRecorder = new FFmpegFrameRecorder(outputUrl, mWidth, mHeight, 1); + frameRecorder.setVideoCodec(AV_CODEC_ID_H264);//28); + frameRecorder.setFormat("flv"); + frameRecorder.setSampleRate(sampleAudioRateInHz); + // Set in the surface changed method + frameRecorder.setFrameRate(frameRate); // 30fps + //frameRecorder.setVideoQuality(0); + + if (RECORD_WITH_AUTIO) { + audioRecordRunnable = new AudioRecordRunnable(); + audioThread = new Thread(audioRecordRunnable); + runAudioThread = true; + } + + if(frame_source == FRAME_FROM_MY_SURFACE){ + mSurfaceTexture = new SurfaceTexture(mTextureId); + mSurface = new Surface(mSurfaceTexture); + mSurfaceTexture.setOnFrameAvailableListener(frameListener, myHandler); + }else if(frame_source == FRAME_FROM_IMAGE_READER){ + mImageReader = ImageReader.newInstance(mWidth, mHeight, PixelFormat.RGBA_8888, MAX_IMAGE_NUMBER); + mSurface = mImageReader.getSurface(); + mImageReader.setOnImageAvailableListener(imageListener, myHandler); + }else { + MediaFormat format = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight); // H.264 Advanced Video Coding + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, + MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + format.setInteger(MediaFormat.KEY_BIT_RATE, 1000000); // 500Kbps, 1Mbps + format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); // 30fps + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); // 2 seconds between I-frames + Log.d(TAG, "created video format: " + format); + mEncoder = MediaCodec.createEncoderByType("video/avc"); + mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + //mEncoder.setOnFrameRenderedListener(mediaFrameListener, myHandler); + mEncoder.setCallback(frameCallback, myHandler); + mSurface = mEncoder.createInputSurface(); + } + virtualDisplay = mediaProjection.createVirtualDisplay("MainScreen", mWidth, mHeight, mDpi, + DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mSurface, null, null); + + return true; + }catch (Exception e){ + Log.e(TAG,"initRtmpRecorder exception "+e+","+Log.getStackTraceString(e)); + return false; + } + } + private void deinitRtmpRecorder() { + Log.d(TAG, "deinitRtmpRecorder "+FRAME_FROM_IMAGE_READER); + if(mRtmpThread != null){ + mRtmpThread.cancel(); + mRtmpThread = null; + } + if(RECORD_WITH_AUTIO) { + runAudioThread = false; + try { + audioThread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + audioRecordRunnable = null; + audioThread = null; + } + + if (virtualDisplay != null) { + mediaProjection.stop(); + virtualDisplay.release(); + virtualDisplay = null; + } + if(frame_source == FRAME_FROM_MY_SURFACE) { + if(mSurfaceTexture != null) { + mSurfaceTexture.release(); + mSurfaceTexture = null; + } + }else if(frame_source == FRAME_FROM_IMAGE_READER){ + if(mImageReader != null){ + mImageReader.close(); + } + }else { + if (mEncoder != null) { + mEncoder.stop(); + mEncoder.release(); + mEncoder = null; + } + } + if (frameRecorder != null && recording) { + recording = false; + try { + frameRecorder.stop(); + frameRecorder.release(); + } catch (FFmpegFrameRecorder.Exception e) { + e.printStackTrace(); + Log.e(TAG, "deinitRtmpRecorder exception "+e+", "+Log.getStackTraceString(e)); + } + frameRecorder = null; + } + } + + + private MediaCodec.Callback frameCallback = new MediaCodec.Callback() { + @Override + public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) { + + } + + @Override + public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo buffer) { + ByteBuffer encodedData = codec.getOutputBuffer(index); + //writeSampleData(mVideoTrackIndex, buffer, encodedData); + if ((buffer.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. + buffer.size = 0; + } + boolean eos = (buffer.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; + if (buffer.size == 0 && !eos) { + encodedData = null; + } else { + if (buffer.presentationTimeUs != 0) { // maybe 0 if eos + if (mVideoPtsOffset == 0) { + mVideoPtsOffset = buffer.presentationTimeUs; + buffer.presentationTimeUs = 0; + } else { + buffer.presentationTimeUs -= mVideoPtsOffset; + } + } + /*if (VERBOSE) + Log.d(TAG, "[" + Thread.currentThread().getId() + "] Got buffer, track=" + track + + ", info: size=" + buffer.size + + ", presentationTimeUs=" + buffer.presentationTimeUs); + if (!eos && mCallback != null) { + mCallback.onRecording(buffer.presentationTimeUs); + }*/ + } + if (encodedData != null) { + encodedData.position(buffer.offset); + encodedData.limit(buffer.offset + buffer.size); + //byte[] bytes = new byte[encodedData.remaining()]; + //encodedData.get(bytes); + /*((ByteBuffer) yuvImage.image[0].position(0)).put(encodedData); + try { + long t = 1000 * (System.currentTimeMillis() - startTime); + Log.v(TAG, "Writing Frame t=" + t + ", ts=" + frameRecorder.getTimestamp()); + if (t > frameRecorder.getTimestamp()) { + frameRecorder.setTimestamp(t); + } + + frameRecorder.record(yuvImage, PixelFormat.); + } catch (FFmpegFrameRecorder.Exception e) { + Log.e(TAG, "RtmpThread exception " + e + "," + Log.getStackTraceString(e)); + }*/ + } + + codec.releaseOutputBuffer(index, true); + } + + @Override + public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) { + + } + + @Override + public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) { + + } + }; + private SurfaceTexture.OnFrameAvailableListener frameListener = new SurfaceTexture.OnFrameAvailableListener() { + + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + Log.v(TAG, "OnFrameAvail"); + synchronized (frameLock){ + frameAvail = true; + frameLock.notifyAll(); + } + } + }; + private ImageReader.OnImageAvailableListener imageListener = new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + Log.v(TAG, "OnImageAvail"); + synchronized (frameLock){ + frameAvail = true; + frameLock.notifyAll(); + } + } + }; + + private MediaCodec.OnFrameRenderedListener mediaFrameListener = new MediaCodec.OnFrameRenderedListener() { + @Override + public void onFrameRendered(@NonNull MediaCodec codec, long presentationTimeUs, long nanoTime) { + Log.v(TAG, "onFrameRendered"); + synchronized (frameLock){ + frameAvail = true; + frameLock.notifyAll(); + } + } + }; + //--------------------------------------------- + // audio thread, gets and encodes audio data + //--------------------------------------------- + class AudioRecordRunnable implements Runnable { + + @Override + public void run() { + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO); + + // Audio + int bufferSize; + ShortBuffer audioData; + int bufferReadResult; + + bufferSize = AudioRecord.getMinBufferSize(sampleAudioRateInHz, + AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); + audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleAudioRateInHz, + AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); + + + audioData = ShortBuffer.allocate(bufferSize); + + Log.d(TAG, "audioRecord startRecording()"); + audioRecord.startRecording(); + + /* ffmpeg_audio encoding loop */ + while (runAudioThread) { + //Log.v(LOG_TAG,"recording? " + recording); + bufferReadResult = audioRecord.read(audioData.array(), 0, audioData.capacity()); + audioData.limit(bufferReadResult); + if (bufferReadResult > 0) { + //Log.v(TAG, "audioRecord bufferReadResult: " + bufferReadResult); + // If "recording" isn't true when start this thread, it never get's set according to this if statement...!!! + // Why? Good question... + if (recording) { + try { + frameRecorder.recordSamples(audioData); + //Log.v(LOG_TAG,"recording " + 1024*i + " to " + 1024*i+1024); + } catch (FFmpegFrameRecorder.Exception e) { + Log.e(TAG, "audioRecord: exception "+e+","+Log.getStackTraceString(e)); + } + } + } + } + Log.v(TAG, "audioRecord Finished"); + + /* encoding finish, release recorder */ + if (audioRecord != null) { + audioRecord.stop(); + audioRecord.release(); + audioRecord = null; + } + } + } + + private final class RtmpThread extends Thread { + boolean running = false; + boolean ended = true; + public void cancel(){ + Log.d(TAG,"cancel scannerthread"); + long t1 = System.currentTimeMillis(); + running = false; + int count = 0; + while(!ended && (count++ <50)){ + try { + Thread.sleep(100); + }catch (Exception e) {} + } + Log.d(TAG,"cancel MarkerThread consume "+(System.currentTimeMillis()-t1)+" ms"); + } + + @Override + public void run() { + running = true; + ended = false; + setName("RtmpThread"); + + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO); + + Log.d(TAG,"RtmpThread started."); + boolean frameavail = false; + while (running) { + synchronized (frameLock){ + try { + frameLock.wait(); + frameavail = frameAvail; + }catch (Exception e){} + frameAvail = false; + } + if(running && frameavail){ + //Log.d(TAG,"RtmpThread check image"); + if(frame_source == FRAME_FROM_MY_SURFACE) { + mSurfaceTexture.updateTexImage(); + Log.d(TAG,"RtmpThread frame updated."); + + }else if(frame_source == FRAME_FROM_IMAGE_READER) { + Image mImage = null; + while((mImage = mImageReader.acquireNextImage())!=null) { + int mWidth = mImage.getWidth(); + int mHeight = mImage.getHeight(); + //Log.d(TAG,"RtmpThread image found " + mWidth+","+mHeight); + if (recording) { + /* + // Four bytes per pixel: width * height * 4. + byte[] rgbaBytes = new byte[mWidth * mHeight * 4]; + // put the data into the rgbaBytes array. + mImage.getPlanes()[0].getBuffer().get(rgbaBytes); + mImage.close(); // Access to the image is no longer needed, release it. + + // Create a yuv byte array: width * height * 1.5 (). + byte[] yuv = new byte[mWidth * mHeight * 3 / 2]; + int ret = YuvUtils.RgbaToI420(Key.ARGB_TO_I420, rgbaBytes, yuv, mWidth, mHeight); + if (ret == 0) { + ((ByteBuffer) yuvImage.image[0].position(0)).put(yuv); + try { + long t = 1000 * (System.currentTimeMillis() - startTime); + Log.v(TAG, "Writing Frame t=" + t + ", ts=" + frameRecorder.getTimestamp()); + if (t > frameRecorder.getTimestamp()) { + frameRecorder.setTimestamp(t); + } + + frameRecorder.record(yuvImage); + } catch (FFmpegFrameRecorder.Exception e) { + Log.e(TAG, "RtmpThread exception " + e + "," + Log.getStackTraceString(e)); + } + }*/ + ((ByteBuffer) yuvImage.image[0].position(0)).put(mImage.getPlanes()[0].getBuffer()); + mImage.close(); + + try { + long t = 1000 * (System.currentTimeMillis() - startTime); + Log.v(TAG, "Writing Frame t=" + t + ", ts=" + frameRecorder.getTimestamp()); + if (t > frameRecorder.getTimestamp()) { + frameRecorder.setTimestamp(t); + } + + frameRecorder.record(yuvImage); + } catch (FFmpegFrameRecorder.Exception e) { + Log.e(TAG, "RtmpThread exception " + e + "," + Log.getStackTraceString(e)); + } + } + } + }else{ + // TBD + } + } + } + ended = true; + Log.d(TAG,"RtmpThread ended."); + } + } + + +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1ef2ff7..85a9c1a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,6 +1,7 @@ - - +