diff --git a/app/src/main/java/net/ossrs/yasea/MainActivity.java b/app/src/main/java/net/ossrs/yasea/MainActivity.java index 85691e7..37ce231 100644 --- a/app/src/main/java/net/ossrs/yasea/MainActivity.java +++ b/app/src/main/java/net/ossrs/yasea/MainActivity.java @@ -4,16 +4,11 @@ import android.app.Activity; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; -import android.hardware.Camera; -import android.hardware.Camera.Size; -import android.media.AudioRecord; -import android.media.MediaRecorder; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.Menu; import android.view.MenuItem; -import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.WindowManager; @@ -21,14 +16,11 @@ import android.widget.Button; import android.widget.EditText; import android.widget.Toast; -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.Random; - import net.ossrs.yasea.rtmp.RtmpPublisher; -public class MainActivity extends Activity implements SurfaceHolder.Callback, Camera.PreviewCallback { +import java.util.Random; + +public class MainActivity extends Activity { private static final String TAG = "Yasea"; Button btnPublish = null; @@ -36,130 +28,12 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback, Ca Button btnRecord = null; Button btnSwitchEncoder = null; - private AudioRecord mic = null; - private boolean aloop = false; - private Thread aworker = null; - - private SurfaceView mCameraView = null; - private Camera mCamera = null; - - private int videoFrameCount; - private long lastTimeMillis; - private int mPreviewRotation = 90; - private int mCamId = Camera.getNumberOfCameras() - 1; // default camera - private byte[] mYuvFrameBuffer = new byte[SrsEncoder.VPREV_WIDTH * SrsEncoder.VPREV_HEIGHT * 3 / 2]; - private String mNotifyMsg; private SharedPreferences sp; private String rtmpUrl = "rtmp://ossrs.net/" + getRandomAlphaString(3) + '/' + getRandomAlphaDigitString(5); private String recPath = Environment.getExternalStorageDirectory().getPath() + "/test.mp4"; - private SrsFlvMuxer flvMuxer = new SrsFlvMuxer(new RtmpPublisher.EventHandler() { - @Override - public void onRtmpConnecting(String msg) { - mNotifyMsg = msg; - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); - } - }); - } - - @Override - public void onRtmpConnected(String msg) { - mNotifyMsg = msg; - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); - } - }); - } - - @Override - public void onRtmpVideoStreaming(String msg) { - } - - @Override - public void onRtmpAudioStreaming(String msg) { - } - - @Override - public void onRtmpStopped(String msg) { - mNotifyMsg = msg; - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); - } - }); - } - - @Override - public void onRtmpDisconnected(String msg) { - mNotifyMsg = msg; - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); - } - }); - } - - @Override - public void onRtmpOutputFps(final double fps) { - Log.i(TAG, String.format("Output Fps: %f", fps)); - } - }); - - private SrsMp4Muxer mp4Muxer = new SrsMp4Muxer(new SrsMp4Muxer.EventHandler() { - @Override - public void onRecordPause(String msg) { - mNotifyMsg = msg; - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); - } - }); - } - - @Override - public void onRecordResume(String msg) { - mNotifyMsg = msg; - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); - } - }); - } - - @Override - public void onRecordStarted(String msg) { - mNotifyMsg = "Recording file: " + msg; - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); - } - }); - } - - @Override - public void onRecordFinished(String msg) { - mNotifyMsg = "MP4 file saved: " + msg; - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); - } - }); - } - }); - - private SrsEncoder mEncoder = new SrsEncoder(flvMuxer, mp4Muxer); + private SrsPublisher mPublisher = new SrsPublisher(); @Override protected void onCreate(Bundle savedInstanceState) { @@ -172,20 +46,19 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback, Ca setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR); // restore data. - sp = getSharedPreferences("SrsPublisher", MODE_PRIVATE); + sp = getSharedPreferences("Yasea", MODE_PRIVATE); rtmpUrl = sp.getString("rtmpUrl", rtmpUrl); // initialize url. final EditText efu = (EditText) findViewById(R.id.url); efu.setText(rtmpUrl); - // for camera, @see https://developer.android.com/reference/android/hardware/Camera.html btnPublish = (Button) findViewById(R.id.publish); btnSwitchCamera = (Button) findViewById(R.id.swCam); btnRecord = (Button) findViewById(R.id.record); btnSwitchEncoder = (Button) findViewById(R.id.swEnc); - mCameraView = (SurfaceView) findViewById(R.id.preview); - mCameraView.getHolder().addCallback(this); + + mPublisher.setSurfaceView((SurfaceView) findViewById(R.id.preview)); btnPublish.setOnClickListener(new View.OnClickListener() { @Override @@ -197,16 +70,10 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback, Ca editor.putString("rtmpUrl", rtmpUrl); editor.commit(); - try { - flvMuxer.start(rtmpUrl); - } catch (IOException e) { - Log.e(TAG, "start FLV muxer failed."); - e.printStackTrace(); - return; - } - flvMuxer.setVideoResolution(mEncoder.VOUT_WIDTH, mEncoder.VOUT_HEIGHT); - - startEncoder(); + mPublisher.setPreviewResolution(1280, 720); + mPublisher.setOutputResolution(384, 640); + mPublisher.setVideoHDMode(); + mPublisher.startPublish(rtmpUrl); if (btnSwitchEncoder.getText().toString().contentEquals("soft enc")) { Toast.makeText(getApplicationContext(), "Use hard encoder", Toast.LENGTH_SHORT).show(); @@ -216,9 +83,8 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback, Ca btnPublish.setText("stop"); btnSwitchEncoder.setEnabled(false); } else if (btnPublish.getText().toString().contentEquals("stop")) { - stopEncoder(); - flvMuxer.stop(); - mp4Muxer.stop(); + mPublisher.stopPublish(); + btnPublish.setText("publish"); btnRecord.setText("record"); btnSwitchEncoder.setEnabled(true); @@ -229,11 +95,8 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback, Ca btnSwitchCamera.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (mCamera != null && mEncoder != null) { - mCamId = (mCamId + 1) % Camera.getNumberOfCameras(); - stopCamera(); - mEncoder.swithCameraFace(); - startCamera(); + if (mPublisher.getNumberOfCameras() > 0) { + mPublisher.switchCameraFace((mPublisher.getCamraId() + 1) % mPublisher.getNumberOfCameras()); } } }); @@ -242,18 +105,14 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback, Ca @Override public void onClick(View v) { if (btnRecord.getText().toString().contentEquals("record")) { - try { - mp4Muxer.record(new File(recPath)); - } catch (IOException e) { - Log.e(TAG, "start MP4 muxer failed."); - e.printStackTrace(); - } + mPublisher.startRecord(recPath); + btnRecord.setText("pause"); } else if (btnRecord.getText().toString().contentEquals("pause")) { - mp4Muxer.pause(); + mPublisher.pauseRecord(); btnRecord.setText("resume"); } else if (btnRecord.getText().toString().contentEquals("resume")) { - mp4Muxer.resume(); + mPublisher.resumeRecord(); btnRecord.setText("pause"); } } @@ -262,18 +121,121 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback, Ca btnSwitchEncoder.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (mEncoder != null) { - if (btnSwitchEncoder.getText().toString().contentEquals("soft enc")) { - mEncoder.swithToSoftEncoder(); - btnSwitchEncoder.setText("hard enc"); - } else if (btnSwitchEncoder.getText().toString().contentEquals("hard enc")) { - mEncoder.swithToHardEncoder(); - btnSwitchEncoder.setText("soft enc"); - } + if (btnSwitchEncoder.getText().toString().contentEquals("soft enc")) { + mPublisher.swithToSoftEncoder(); + btnSwitchEncoder.setText("hard enc"); + } else if (btnSwitchEncoder.getText().toString().contentEquals("hard enc")) { + mPublisher.swithToHardEncoder(); + btnSwitchEncoder.setText("soft enc"); } } }); + mPublisher.setPublishEventHandler(new RtmpPublisher.EventHandler() { + @Override + public void onRtmpConnecting(String msg) { + mNotifyMsg = msg; + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public void onRtmpConnected(String msg) { + mNotifyMsg = msg; + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public void onRtmpVideoStreaming(String msg) { + } + + @Override + public void onRtmpAudioStreaming(String msg) { + } + + @Override + public void onRtmpStopped(String msg) { + mNotifyMsg = msg; + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public void onRtmpDisconnected(String msg) { + mNotifyMsg = msg; + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public void onRtmpOutputFps(final double fps) { + Log.i(TAG, String.format("Output Fps: %f", fps)); + } + }); + + mPublisher.setRecordEventHandler(new SrsMp4Muxer.EventHandler() { + @Override + public void onRecordPause(String msg) { + mNotifyMsg = msg; + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public void onRecordResume(String msg) { + mNotifyMsg = msg; + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public void onRecordStarted(String msg) { + mNotifyMsg = "Recording file: " + msg; + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public void onRecordFinished(String msg) { + mNotifyMsg = "MP4 file saved: " + msg; + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_SHORT).show(); + } + }); + } + }); + Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable ex) { @@ -282,9 +244,8 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback, Ca @Override public void run() { Toast.makeText(getApplicationContext(), mNotifyMsg, Toast.LENGTH_LONG).show(); - stopEncoder(); - flvMuxer.stop(); - mp4Muxer.stop(); + mPublisher.stopPublish(); + mPublisher.stopRecord(); btnPublish.setText("publish"); btnRecord.setText("record"); btnSwitchEncoder.setEnabled(true); @@ -316,175 +277,42 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback, Ca return super.onOptionsItemSelected(item); } - private void startCamera() { - if (mCamera != null) { - Log.d(TAG, "start camera, already started. return"); - return; - } - if (mCamId > (Camera.getNumberOfCameras() - 1) || mCamId < 0) { - Log.e(TAG, "####### start camera failed, inviald params, camera No.="+ mCamId); - return; - } - - mCamera = Camera.open(mCamId); - Camera.Parameters params = mCamera.getParameters(); - /* preview size */ - Size size = mCamera.new Size(SrsEncoder.VPREV_WIDTH, SrsEncoder.VPREV_HEIGHT); - if (!params.getSupportedPreviewSizes().contains(size)) { - Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), - new IllegalArgumentException(String.format("Unsupported preview size %dx%d", size.width, size.height))); - } - - /* picture size */ - if (!params.getSupportedPictureSizes().contains(size)) { - Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), - new IllegalArgumentException(String.format("Unsupported picture size %dx%d", size.width, size.height))); - } - - /***** set parameters *****/ - //params.set("orientation", "portrait"); - //params.set("orientation", "landscape"); - //params.setRotation(90); - params.setPictureSize(SrsEncoder.VPREV_WIDTH, SrsEncoder.VPREV_HEIGHT); - params.setPreviewSize(SrsEncoder.VPREV_WIDTH, SrsEncoder.VPREV_HEIGHT); - int[] range = findClosestFpsRange(SrsEncoder.VFPS, params.getSupportedPreviewFpsRange()); - params.setPreviewFpsRange(range[0], range[1]); - params.setPreviewFormat(SrsEncoder.VFORMAT); - params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); - params.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO); - params.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO); - if (!params.getSupportedFocusModes().isEmpty()) { - params.setFocusMode(params.getSupportedFocusModes().get(0)); - } - mCamera.setParameters(params); - - mCamera.setDisplayOrientation(mPreviewRotation); - - mCamera.addCallbackBuffer(mYuvFrameBuffer); - mCamera.setPreviewCallbackWithBuffer(this); - try { - mCamera.setPreviewDisplay(mCameraView.getHolder()); - } catch (IOException e) { - e.printStackTrace(); - } - mCamera.startPreview(); - } - - private void stopCamera() { - if (mCamera != null) { - // need to SET NULL CB before stop preview!!! - mCamera.setPreviewCallback(null); - mCamera.stopPreview(); - mCamera.release(); - mCamera = null; - } - } - - private void onGetYuvFrame(byte[] data) { - // Calculate YUV sampling FPS - if (videoFrameCount == 0) { - lastTimeMillis = System.nanoTime() / 1000000; - videoFrameCount++; - } else { - if (++videoFrameCount >= 48) { - long diffTimeMillis = System.nanoTime() / 1000000 - lastTimeMillis; - Log.i(TAG, String.format("Sampling fps: %f", (double) videoFrameCount * 1000 / diffTimeMillis)); - videoFrameCount = 0; - } - } - mEncoder.onGetYuvFrame(data); - } - @Override - public void onPreviewFrame(byte[] data, Camera c) { - onGetYuvFrame(data); - c.addCallbackBuffer(mYuvFrameBuffer); - } - - private void onGetPcmFrame(byte[] pcmBuffer, int size) { - mEncoder.onGetPcmFrame(pcmBuffer, size); + protected void onResume() { + super.onResume(); + final Button btn = (Button) findViewById(R.id.publish); + btn.setEnabled(true); + mPublisher.resumeRecord(); } - private void startAudio() { - if (mic != null) { - return; - } - - int bufferSize = 2 * AudioRecord.getMinBufferSize(SrsEncoder.ASAMPLERATE, SrsEncoder.ACHANNEL, SrsEncoder.AFORMAT); - mic = new AudioRecord(MediaRecorder.AudioSource.MIC, SrsEncoder.ASAMPLERATE, SrsEncoder.ACHANNEL, SrsEncoder.AFORMAT, bufferSize); - mic.startRecording(); - - byte pcmBuffer[] = new byte[4096]; - while (aloop && !Thread.interrupted()) { - int size = mic.read(pcmBuffer, 0, pcmBuffer.length); - if (size <= 0) { - Log.e(TAG, "***** audio ignored, no data to read."); - break; - } - onGetPcmFrame(pcmBuffer, size); - } + @Override + protected void onPause() { + super.onPause(); + mPublisher.pauseRecord(); } - private void stopAudio() { - aloop = false; - if (aworker != null) { - Log.i(TAG, "stop audio worker thread"); - aworker.interrupt(); - try { - aworker.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - aworker.interrupt(); - } - aworker = null; - } - - if (mic != null) { - mic.setRecordPositionUpdateListener(null); - mic.stop(); - mic.release(); - mic = null; - } + @Override + protected void onDestroy() { + super.onDestroy(); + mPublisher.stopPublish(); + mPublisher.stopRecord(); } - private void startEncoder() { - if (!mEncoder.start()) { - return; + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { + mPublisher.setPreviewRotation(90); + } else if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { + mPublisher.setPreviewRotation(0); } - - startCamera(); - - aworker = new Thread(new Runnable() { - @Override - public void run() { - android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); - startAudio(); - } - }); - aloop = true; - aworker.start(); - } - - private void stopEncoder() { - stopAudio(); - stopCamera(); - mEncoder.stop(); - } - - private static int[] findClosestFpsRange(int expectedFps, List fpsRanges) { - expectedFps *= 1000; - int[] closestRange = fpsRanges.get(0); - int measure = Math.abs(closestRange[0] - expectedFps) + Math.abs(closestRange[1] - expectedFps); - for (int[] range : fpsRanges) { - if (range[0] <= expectedFps && range[1] >= expectedFps) { - int curMeasure = Math.abs(range[0] - expectedFps) + Math.abs(range[1] - expectedFps); - if (curMeasure < measure) { - closestRange = range; - measure = curMeasure; - } - } + mPublisher.stopEncode(); + mPublisher.stopRecord(); + btnRecord.setText("record"); + mPublisher.setScreenOrientation(newConfig.orientation); + if (btnPublish.getText().toString().contentEquals("stop")) { + mPublisher.startEncode(); } - return closestRange; } private static String getRandomAlphaString(int length) { @@ -508,65 +336,4 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback, Ca } return sb.toString(); } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - Log.d(TAG, "surfaceChanged"); - } - - @Override - public void surfaceCreated(SurfaceHolder arg0) { - Log.d(TAG, "surfaceCreated"); - if (mCamera != null) { - try { - mCamera.setPreviewDisplay(mCameraView.getHolder()); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - @Override - public void surfaceDestroyed(SurfaceHolder arg0) { - Log.d(TAG, "surfaceDestroyed"); - } - - @Override - protected void onResume() { - super.onResume(); - final Button btn = (Button) findViewById(R.id.publish); - btn.setEnabled(true); - mp4Muxer.resume(); - } - - @Override - protected void onPause() { - super.onPause(); - mp4Muxer.pause(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - stopEncoder(); - flvMuxer.stop(); - mp4Muxer.stop(); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { - mPreviewRotation = 90; - } else if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { - mPreviewRotation = 0; - } - stopEncoder(); - mp4Muxer.stop(); - btnRecord.setText("record"); - mEncoder.setScreenOrientation(newConfig.orientation); - if (btnPublish.getText().toString().contentEquals("stop")) { - startEncoder(); - } - } } diff --git a/app/src/main/java/net/ossrs/yasea/SrsEncoder.java b/app/src/main/java/net/ossrs/yasea/SrsEncoder.java index 2b6d5a1..57f6711 100644 --- a/app/src/main/java/net/ossrs/yasea/SrsEncoder.java +++ b/app/src/main/java/net/ossrs/yasea/SrsEncoder.java @@ -1,7 +1,6 @@ package net.ossrs.yasea; import android.content.res.Configuration; -import android.graphics.ImageFormat; import android.media.AudioFormat; import android.media.MediaCodec; import android.media.MediaCodecInfo; @@ -20,22 +19,21 @@ public class SrsEncoder { public static final String VCODEC = "video/avc"; public static final String ACODEC = "audio/mp4a-latm"; - public static final int VPREV_WIDTH = 1280; - public static final int VPREV_HEIGHT = 720; - public static final int VOUT_WIDTH = 384; - public static final int VOUT_HEIGHT = 640; - public static int vOutWidth = VOUT_WIDTH; // Note: the stride of resolution must be set as 16x for hard encoding with some chip like MTK - public static int vOutHeight = VOUT_HEIGHT; // Since Y component is quadruple size as U and V component, the stride must be set as 32x - public static final int VBITRATE = 500 * 1000; // 500kbps + public static String x264Preset = "veryfast"; + public static int vPrevWidth = 1280; + public static int vPrevHeight = 720; + public static int vPortraitWidth = 384; + public static int vPortraitHeight = 640; + public static int vOutWidth = 384; // Note: the stride of resolution must be set as 16x for hard encoding with some chip like MTK + public static int vOutHeight = 640; // Since Y component is quadruple size as U and V component, the stride must be set as 32x + public static int vBitrate = 500 * 1000; // 500kbps public static final int VFPS = 24; public static final int VGOP = 48; - public static final int VFORMAT = ImageFormat.NV21; public static final int ASAMPLERATE = 44100; public static final int ACHANNEL = AudioFormat.CHANNEL_IN_STEREO; - public static final int AFORMAT = AudioFormat.ENCODING_PCM_16BIT; public static final int ABITRATE = 32 * 1000; // 32kbps - private volatile int mOrientation = Configuration.ORIENTATION_PORTRAIT; + private int mOrientation = Configuration.ORIENTATION_PORTRAIT; private SrsFlvMuxer flvMuxer; private SrsMp4Muxer mp4Muxer; @@ -69,14 +67,23 @@ public class SrsEncoder { // NV16 -> YUV422SP yyyy uv uv // YUY2 -> YUV422SP yuyv yuyv - public SrsEncoder(SrsFlvMuxer flvMuxer, SrsMp4Muxer mp4Muxer) { + public SrsEncoder() { + mVideoColorFormat = chooseVideoEncoder(); + } + + public void setFlvMuxer(SrsFlvMuxer flvMuxer) { this.flvMuxer = flvMuxer; - this.mp4Muxer = mp4Muxer; + } - mVideoColorFormat = chooseVideoEncoder(); + public void setMp4Muxer(SrsMp4Muxer mp4Muxer) { + this.mp4Muxer = mp4Muxer; } public boolean start() { + if (flvMuxer == null || mp4Muxer == null) { + return false; + } + // the referent PTS for video and audio encoder. mPresentTimeUs = System.nanoTime() / 1000; @@ -91,13 +98,13 @@ public class SrsEncoder { setEncoderResolution(vOutWidth, vOutHeight); setEncoderFps(VFPS); setEncoderGop(VGOP); - // Unfortunately for some android phone, the output fps is less than 10 limited by the + // Unfortunately for some android phone, the output fps is less than 10 lSrsted by the // capacity of poor cheap chips even with x264. So for the sake of quick appearance of // the first picture on the player, a spare lower GOP value is suggested. But note that // lower GOP will produce more I frames and therefore more streaming data flow. // setEncoderGop(15); - setEncoderBitrate(VBITRATE); - setEncoderPreset("veryfast"); + setEncoderBitrate(vBitrate); + setEncoderPreset(x264Preset); if (useSoftEncoder && !openSoftEncoder()) { return false; @@ -139,7 +146,7 @@ public class SrsEncoder { MediaFormat videoFormat = MediaFormat.createVideoFormat(VCODEC, vOutWidth, vOutHeight); videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mVideoColorFormat); videoFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0); - videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, VBITRATE); + videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, vBitrate); videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, VFPS); videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, VGOP / VFPS); vencoder.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); @@ -189,16 +196,65 @@ public class SrsEncoder { useSoftEncoder = false; } + public boolean isSoftEncoder() { + return useSoftEncoder; + } + + public void setPreviewResolution(int width, int height) { + vPrevWidth = width; + vPrevHeight = height; + } + + public void setPortraitResolution(int width, int height) { + vOutWidth = width; + vOutHeight = height; + vPortraitWidth = width; + vPortraitHeight = height; + } + + public void setLandscapeResolution(int width, int height) { + vOutWidth = width; + vOutHeight = height; + vPortraitWidth = height; + vPortraitHeight = width; + } + + public void setVideoHDMode() { + vBitrate = 1200 * 1000; // 1200 kbps + x264Preset = "veryfast"; + } + + public void setVideoSmoothMode() { + vBitrate = 500 * 1000; // 500 kbps + x264Preset = "superfast"; + } + + public int getPreviewWidth() { + return vPrevWidth; + } + + public int getPreviewHeight() { + return vPrevHeight; + } + + public int getOutputWidth() { + return vOutWidth; + } + + public int getOutputHeight() { + return vOutHeight; + } + public void setScreenOrientation(int orientation) { mOrientation = orientation; if (mOrientation == Configuration.ORIENTATION_PORTRAIT) { - vOutWidth = VOUT_WIDTH; - vOutHeight = VOUT_HEIGHT; + vOutWidth = vPortraitWidth; + vOutHeight = vPortraitHeight; } else if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) { - vOutWidth = VOUT_HEIGHT; - vOutHeight = VOUT_WIDTH; + vOutWidth = vPortraitHeight; + vOutHeight = vPortraitWidth; } - + // Note: the stride of resolution must be set as 16x for hard encoding with some chip like MTK // Since Y component is quadruple size as U and V component, the stride must be set as 32x if (!useSoftEncoder && vOutWidth % 32 != 0 || vOutHeight % 32 != 0) { @@ -239,7 +295,6 @@ public class SrsEncoder { vebi.offset = 0; vebi.size = es.length; vebi.presentationTimeUs = pts; - //vebi.presentationTimeUs = System.nanoTime() / 1000 - mPresentTimeUs; vebi.flags = isKeyFrame ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0; onEncodedAnnexbFrame(bb, vebi); } @@ -324,18 +379,18 @@ public class SrsEncoder { if (mCameraFaceFront) { switch (mVideoColorFormat) { case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: - return NV21ToI420(data, VPREV_WIDTH, VPREV_HEIGHT, true, 270); + return NV21ToI420(data, vPrevWidth, vPrevHeight, true, 270); case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: - return NV21ToNV12(data, VPREV_WIDTH, VPREV_HEIGHT, true, 270); + return NV21ToNV12(data, vPrevWidth, vPrevHeight, true, 270); default: throw new IllegalStateException("Unsupported color format!"); } } else { switch (mVideoColorFormat) { case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: - return NV21ToI420(data, VPREV_WIDTH, VPREV_HEIGHT, false, 90); + return NV21ToI420(data, vPrevWidth, vPrevHeight, false, 90); case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: - return NV21ToNV12(data, VPREV_WIDTH, VPREV_HEIGHT, false, 90); + return NV21ToNV12(data, vPrevWidth, vPrevHeight, false, 90); default: throw new IllegalStateException("Unsupported color format!"); } @@ -346,18 +401,18 @@ public class SrsEncoder { if (mCameraFaceFront) { switch (mVideoColorFormat) { case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: - return NV21ToI420(data, VPREV_WIDTH, VPREV_HEIGHT, true, 0); + return NV21ToI420(data, vPrevWidth, vPrevHeight, true, 0); case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: - return NV21ToNV12(data, VPREV_WIDTH, VPREV_HEIGHT, true, 0); + return NV21ToNV12(data, vPrevWidth, vPrevHeight, true, 0); default: throw new IllegalStateException("Unsupported color format!"); } } else { switch (mVideoColorFormat) { case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: - return NV21ToI420(data, VPREV_WIDTH, VPREV_HEIGHT, false, 0); + return NV21ToI420(data, vPrevWidth, vPrevHeight, false, 0); case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: - return NV21ToNV12(data, VPREV_WIDTH, VPREV_HEIGHT, false, 0); + return NV21ToNV12(data, vPrevWidth, vPrevHeight, false, 0); default: throw new IllegalStateException("Unsupported color format!"); } @@ -366,17 +421,17 @@ public class SrsEncoder { private void swPortraitYuvFrame(byte[] data, long pts) { if (mCameraFaceFront) { - NV21SoftEncode(data, VPREV_WIDTH, VPREV_HEIGHT, true, 270, pts); + NV21SoftEncode(data, vPrevWidth, vPrevHeight, true, 270, pts); } else { - NV21SoftEncode(data, VPREV_WIDTH, VPREV_HEIGHT, false, 90, pts); + NV21SoftEncode(data, vPrevWidth, vPrevHeight, false, 90, pts); } } private void swLandscapeYuvFrame(byte[] data, long pts) { if (mCameraFaceFront) { - NV21SoftEncode(data, VPREV_WIDTH, VPREV_HEIGHT, true, 0, pts); + NV21SoftEncode(data, vPrevWidth, vPrevHeight, true, 0, pts); } else { - NV21SoftEncode(data, VPREV_WIDTH, VPREV_HEIGHT, false, 0, pts); + NV21SoftEncode(data, vPrevWidth, vPrevHeight, false, 0, pts); } } diff --git a/app/src/main/java/net/ossrs/yasea/SrsPublisher.java b/app/src/main/java/net/ossrs/yasea/SrsPublisher.java new file mode 100644 index 0000000..9a045ba --- /dev/null +++ b/app/src/main/java/net/ossrs/yasea/SrsPublisher.java @@ -0,0 +1,342 @@ +package net.ossrs.yasea; + +import android.graphics.ImageFormat; +import android.hardware.Camera; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import net.ossrs.yasea.rtmp.RtmpPublisher; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * Created by Leo Ma on 2016/7/25. + */ +public class SrsPublisher implements SurfaceHolder.Callback, Camera.PreviewCallback { + private static final String TAG = "SrsPublisher"; + + private AudioRecord mic; + private boolean aloop = false; + private Thread aworker; + + private SurfaceView mCameraView; + private Camera mCamera; + + private int videoFrameCount; + private long lastTimeMillis; + private int mPreviewRotation = 90; + private int mCamId = Camera.getNumberOfCameras() - 1; + private double mSamplingFps; + private byte[] mYuvPreviewFrame; + + private SrsFlvMuxer mFlvMuxer; + private SrsMp4Muxer mMp4Muxer; + private SrsEncoder mEncoder = new SrsEncoder(); + + public SrsPublisher() { + } + + public void startEncode() { + if (!mEncoder.start()) { + return; + } + + startCamera(); + + aworker = new Thread(new Runnable() { + @Override + public void run() { + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); + startAudio(); + } + }); + aloop = true; + aworker.start(); + } + + public void stopEncode() { + stopAudio(); + stopCamera(); + mEncoder.stop(); + } + + public void startPublish(String rtmpUrl) { + if (mFlvMuxer != null) { + try { + mFlvMuxer.start(rtmpUrl); + } catch (IOException e) { + e.printStackTrace(); + return; + } + mFlvMuxer.setVideoResolution(mEncoder.getOutputWidth(), mEncoder.getOutputHeight()); + + startEncode(); + } + } + + public void stopPublish() { + if (mFlvMuxer != null) { + stopEncode(); + mFlvMuxer.stop(); + } + } + + public void startRecord(String recPath) { + if (mMp4Muxer != null) { + try { + mMp4Muxer.record(new File(recPath)); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void stopRecord() { + if (mMp4Muxer != null) { + mMp4Muxer.stop(); + } + } + + public void pauseRecord() { + if (mMp4Muxer != null) { + mMp4Muxer.pause(); + } + } + + public void resumeRecord() { + if (mMp4Muxer != null) { + mMp4Muxer.resume(); + } + } + + public void swithToSoftEncoder() { + mEncoder.swithToSoftEncoder(); + } + + public void swithToHardEncoder() { + mEncoder.swithToHardEncoder(); + } + + public boolean isSoftEncoder() { + return mEncoder.isSoftEncoder(); + } + + public double getmSamplingFps() { + return mSamplingFps; + } + + public int getCamraId() { + return mCamId; + } + + public int getNumberOfCameras() { + return mCamera != null ? mCamera.getNumberOfCameras() : -1; + } + + public void setPreviewResolution(int width, int height) { + mEncoder.setPreviewResolution(width, height); + } + + public void setOutputResolution(int width, int height) { + mEncoder.setPortraitResolution(width, height); + } + + public void setScreenOrientation(int orientation) { + mEncoder.setScreenOrientation(orientation); + } + + public void setPreviewRotation(int rotation) { + mPreviewRotation = rotation; + } + + public void setVideoHDMode() { + mEncoder.setVideoHDMode(); + } + + public void setVideoSmoothMode() { + mEncoder.setVideoSmoothMode(); + } + + public void switchCameraFace(int id) { + mCamId = id; + stopCamera(); + mEncoder.swithCameraFace(); + startCamera(); + } + + private void startCamera() { + if (mCamera != null) { + return; + } + if (mCamId > (Camera.getNumberOfCameras() - 1) || mCamId < 0) { + return; + } + + mCamera = Camera.open(mCamId); + + Camera.Parameters params = mCamera.getParameters(); + Camera.Size size = mCamera.new Size(mEncoder.getPreviewWidth(), mEncoder.getPreviewHeight()); + if (!params.getSupportedPreviewSizes().contains(size) || !params.getSupportedPictureSizes().contains(size)) { + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), + new IllegalArgumentException(String.format("Unsupported resolution %dx%d", size.width, size.height))); + } + + mYuvPreviewFrame = new byte[mEncoder.getPreviewWidth() * mEncoder.getPreviewHeight() * 3 / 2]; + + /***** set parameters *****/ + //params.set("orientation", "portrait"); + //params.set("orientation", "landscape"); + //params.setRotation(90); + params.setPictureSize(mEncoder.getPreviewWidth(), mEncoder.getPreviewHeight()); + params.setPreviewSize(mEncoder.getPreviewWidth(), mEncoder.getPreviewHeight()); + int[] range = findClosestFpsRange(SrsEncoder.VFPS, params.getSupportedPreviewFpsRange()); + params.setPreviewFpsRange(range[0], range[1]); + params.setPreviewFormat(ImageFormat.NV21); + params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); + params.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO); + params.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO); + if (!params.getSupportedFocusModes().isEmpty()) { + params.setFocusMode(params.getSupportedFocusModes().get(0)); + } + mCamera.setParameters(params); + + mCamera.setDisplayOrientation(mPreviewRotation); + + mCamera.addCallbackBuffer(mYuvPreviewFrame); + mCamera.setPreviewCallbackWithBuffer(this); + try { + if (mCameraView != null){ + mCamera.setPreviewDisplay(mCameraView.getHolder()); + } + } catch (IOException e) { + e.printStackTrace(); + } + mCamera.startPreview(); + } + + private static int[] findClosestFpsRange(int expectedFps, List fpsRanges) { + expectedFps *= 1000; + int[] closestRange = fpsRanges.get(0); + int measure = Math.abs(closestRange[0] - expectedFps) + Math.abs(closestRange[1] - expectedFps); + for (int[] range : fpsRanges) { + if (range[0] <= expectedFps && range[1] >= expectedFps) { + int curMeasure = Math.abs(range[0] - expectedFps) + Math.abs(range[1] - expectedFps); + if (curMeasure < measure) { + closestRange = range; + measure = curMeasure; + } + } + } + return closestRange; + } + + private void stopCamera() { + if (mCamera != null) { + // need to SET NULL CB before stop preview!!! + mCamera.setPreviewCallback(null); + mCamera.stopPreview(); + mCamera.release(); + mCamera = null; + } + } + + private void onGetYuvFrame(byte[] data) { + // Calculate YUV sampling FPS + if (videoFrameCount == 0) { + lastTimeMillis = System.nanoTime() / 1000000; + videoFrameCount++; + } else { + if (++videoFrameCount >= 48) { + long diffTimeMillis = System.nanoTime() / 1000000 - lastTimeMillis; + mSamplingFps = (double) videoFrameCount * 1000 / diffTimeMillis; + videoFrameCount = 0; + } + } + mEncoder.onGetYuvFrame(data); + } + + private void startAudio() { + if (mic != null) { + return; + } + + int bufferSize = 2 * AudioRecord.getMinBufferSize(SrsEncoder.ASAMPLERATE, SrsEncoder.ACHANNEL, AudioFormat.ENCODING_PCM_16BIT); + mic = new AudioRecord(MediaRecorder.AudioSource.MIC, SrsEncoder.ASAMPLERATE, SrsEncoder.ACHANNEL, AudioFormat.ENCODING_PCM_16BIT, bufferSize); + mic.startRecording(); + + byte pcmBuffer[] = new byte[4096]; + while (aloop && !Thread.interrupted()) { + int size = mic.read(pcmBuffer, 0, pcmBuffer.length); + if (size <= 0) { + break; + } + mEncoder.onGetPcmFrame(pcmBuffer, size); + } + } + + private void stopAudio() { + aloop = false; + if (aworker != null) { + aworker.interrupt(); + try { + aworker.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + aworker.interrupt(); + } + aworker = null; + } + + if (mic != null) { + mic.setRecordPositionUpdateListener(null); + mic.stop(); + mic.release(); + mic = null; + } + } + + public void setPublishEventHandler(RtmpPublisher.EventHandler handler) { + mFlvMuxer = new SrsFlvMuxer(handler); + mEncoder.setFlvMuxer(mFlvMuxer); + } + + public void setRecordEventHandler(SrsMp4Muxer.EventHandler handler) { + mMp4Muxer = new SrsMp4Muxer(handler); + mEncoder.setMp4Muxer(mMp4Muxer); + } + + public void setSurfaceView(SurfaceView surfaceView) { + mCameraView = surfaceView; + mCameraView.getHolder().addCallback(this); + } + + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + onGetYuvFrame(data); + camera.addCallbackBuffer(mYuvPreviewFrame); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + } + + @Override + public void surfaceCreated(SurfaceHolder arg0) { + if (mCamera != null) { + try { + mCamera.setPreviewDisplay(mCameraView.getHolder()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder arg0) { + } +}