package org.easydarwin.video; import android.annotation.TargetApi; import android.content.Context; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.media.MediaFormat; import android.os.Build; import android.os.Bundle; import android.os.Process; import android.os.ResultReceiver; import android.os.SystemClock; import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; import android.view.Surface; import android.view.TextureView; import org.easydarwin.audio.AudioCodec; import org.easydarwin.audio.EasyAACMuxer; import org.easydarwin.sw.JNIUtil; import org.easydarwin.util.CodecSpecificDataUtil; import org.easydarwin.util.TextureLifecycler; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Comparator; import java.util.PriorityQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import static android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED; import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible; import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar; import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar; import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar; import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar; import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar; import static org.easydarwin.util.CodecSpecificDataUtil.AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE; import static org.easydarwin.video.Client.TRANSTYPE_TCP; import static org.easydarwin.video.EasyMuxer2.VIDEO_TYPE_H264; import static org.easydarwin.video.EasyMuxer2.VIDEO_TYPE_H265; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.OnLifecycleEvent; /** * Created by John on 2016/3/17. */ public class EasyPlayerClient implements Client.SourceCallBack { private static final long LEAST_FRAME_INTERVAL = 10000l; /* 视频编码 */ public static final int EASY_SDK_VIDEO_CODEC_H264 = 0x1C; /* H264 */ public static final int EASY_SDK_VIDEO_CODEC_H265 = 0x48323635; /* H265 */ public static final int EASY_SDK_VIDEO_CODEC_MJPEG = 0x08; /* MJPEG */ public static final int EASY_SDK_VIDEO_CODEC_MPEG4 = 0x0D; /* MPEG4 */ /* 音频编码 */ public static final int EASY_SDK_AUDIO_CODEC_AAC = 0x15002; /* AAC */ public static final int EASY_SDK_AUDIO_CODEC_G711U = 0x10006; /* G711 ulaw */ public static final int EASY_SDK_AUDIO_CODEC_G711A = 0x10007; /* G711 alaw */ public static final int EASY_SDK_AUDIO_CODEC_G726 = 0x1100B; /* G726 */ /** * 表示视频显示出来了 */ public static final int RESULT_VIDEO_DISPLAYED = 01; /** * 表示视频的解码方式 */ public static final String KEY_VIDEO_DECODE_TYPE = "video-decode-type"; /** * 表示视频的尺寸获取到了。具体尺寸见 EXTRA_VIDEO_WIDTH、EXTRA_VIDEO_HEIGHT */ public static final int RESULT_VIDEO_SIZE = 02; public static final int RESULT_TIMEOUT = 03; public static final int RESULT_EVENT = 04; public static final int RESULT_UNSUPPORTED_VIDEO = 05; public static final int RESULT_UNSUPPORTED_AUDIO = 06; public static final int RESULT_RECORD_BEGIN = 7; public static final int RESULT_RECORD_END = 8; /** * 表示第一帧数据已经收到 */ public static final int RESULT_FRAME_RECVED = 9; private static final String TAG = EasyPlayerClient.class.getSimpleName(); /** * 表示视频的宽度 */ public static final String EXTRA_VIDEO_WIDTH = "extra-video-width"; /** * 表示视频的高度 */ public static final String EXTRA_VIDEO_HEIGHT = "extra-video-height"; private static final int NAL_VPS = 32; private static final int NAL_SPS = 33; private static final int NAL_PPS = 34; private Surface mSurface; private final TextureLifecycler lifecycler; private volatile Thread mThread, mAudioThread; private final ResultReceiver mRR; private Client mClient; private boolean mAudioEnable = true; private volatile long mReceivedDataLength; private AudioTrack mAudioTrack; private String mRecordingPath; private EasyAACMuxer mObject; private EasyMuxer2 muxer2; private Client.MediaInfo mMediaInfo; private short mHeight = 0; short mWidth = 0; private ByteBuffer mCSD0; private ByteBuffer mCSD1; private final I420DataCallback i420callback; private boolean mMuxerWaitingKeyVideo; /** * -1 表示暂停中,0表示正常录像中,1表示恢复中。 */ private int mRecordingStatus; private long muxerPausedMillis = 0L; private long mMuxerCuttingMillis = 0L; // private RtmpClient mRTMPClient = new RtmpClient(); public boolean isRecording() { return !TextUtils.isEmpty(mRecordingPath); } private static class FrameInfoQueue extends PriorityQueue { public static final int CAPACITY = 500; public static final int INITIAL_CAPACITY = 300; public FrameInfoQueue() { super(INITIAL_CAPACITY, new Comparator() { @Override public int compare(Client.FrameInfo frameInfo, Client.FrameInfo t1) { return (int) (frameInfo.stamp - t1.stamp); } }); } final ReentrantLock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notVideo = lock.newCondition(); final Condition notAudio = lock.newCondition(); @Override public int size() { lock.lock(); try { return super.size(); } finally { lock.unlock(); } } @Override public void clear() { lock.lock(); try { int size = super.size(); super.clear(); int k = size; for (; k > 0 && lock.hasWaiters(notFull); k--) { notFull.signal(); } } finally { lock.unlock(); } } public void put(Client.FrameInfo x) throws InterruptedException { lock.lockInterruptibly(); try { int size; while ((size = super.size()) == CAPACITY) { Log.v(TAG, "queue full:" + CAPACITY); notFull.await(); } offer(x); // Log.d(TAG, String.format("queue size : " + size)); // 这里是乱序的。并非只有空的queue才丢到首位。因此不能做限制 if (size == 0) { if (x.audio) { notAudio.signal(); } else { notVideo.signal(); } } } finally { lock.unlock(); } } public Client.FrameInfo takeVideoFrame() throws InterruptedException { lock.lockInterruptibly(); try { while (true) { Client.FrameInfo x = peek(); if (x == null) { notVideo.await(); } else { if (!x.audio) { remove(); notFull.signal(); notAudio.signal(); return x; } else { notVideo.await(); } } } } finally { lock.unlock(); } } public Client.FrameInfo takeVideoFrame(long ms) throws InterruptedException { lock.lockInterruptibly(); try { while (true) { Client.FrameInfo x = peek(); if (x == null) { if (!notVideo.await(ms, TimeUnit.MILLISECONDS)) return null; } else { if (!x.audio) { remove(); notFull.signal(); notAudio.signal(); return x; } else { notVideo.await(); } } } } finally { lock.unlock(); } } public Client.FrameInfo takeAudioFrame() throws InterruptedException { lock.lockInterruptibly(); try { while (true) { Client.FrameInfo x = peek(); if (x == null) { notAudio.await(); } else { if (x.audio) { remove(); notFull.signal(); notVideo.signal(); return x; } else { notAudio.await(); } } } } finally { lock.unlock(); } } } private FrameInfoQueue mQueue = new FrameInfoQueue(); private final Context mContext; /** * 最新的视频时间戳 */ private volatile long mNewestStample; private boolean mWaitingKeyFrame; private boolean mTimeout; private boolean mNotSupportedVideoCB, mNotSupportedAudioCB; /** * 创建SDK对象 * * @param context 上下文对象 * @param surface 显示视频用的surface */ public EasyPlayerClient(Context context, Surface surface, ResultReceiver receiver) { this(context, surface, receiver, null); } /** * 创建SDK对象 * * @param context 上下文对象 * @param surface 显示视频用的surface */ public EasyPlayerClient(Context context, Surface surface, ResultReceiver receiver, I420DataCallback callback) { mSurface = surface; mContext = context; mRR = receiver; i420callback = callback; lifecycler = null; } public EasyPlayerClient(Context context, final TextureView view, ResultReceiver receiver, I420DataCallback callback) { lifecycler = new TextureLifecycler(view); mContext = context; mRR = receiver; i420callback = callback; LifecycleObserver observer1 = new LifecycleObserver() { @OnLifecycleEvent(value = Lifecycle.Event.ON_DESTROY) public void destory() { stop(); mSurface.release(); mSurface = null; } @OnLifecycleEvent(value = Lifecycle.Event.ON_CREATE) private void create() { mSurface = new Surface(view.getSurfaceTexture()); } }; lifecycler.getLifecycle().addObserver(observer1); if (context instanceof LifecycleOwner) { LifecycleObserver observer = new LifecycleObserver() { @OnLifecycleEvent(value = Lifecycle.Event.ON_DESTROY) public void destory() { stop(); } @OnLifecycleEvent(value = Lifecycle.Event.ON_PAUSE) private void pause() { EasyPlayerClient.this.pause(); } @OnLifecycleEvent(value = Lifecycle.Event.ON_RESUME) private void resume() { EasyPlayerClient.this.resume(); } }; ((LifecycleOwner) context).getLifecycle().addObserver(observer); } } /** * 启动播放 * * @param url * @return */ public void play(final String url) { if (lifecycler.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.CREATED)) { start(url, TRANSTYPE_TCP, 0, Client.EASY_SDK_VIDEO_FRAME_FLAG | Client.EASY_SDK_AUDIO_FRAME_FLAG, "", "", null); } else { lifecycler.getLifecycle().addObserver(new LifecycleObserver() { @OnLifecycleEvent(value = Lifecycle.Event.ON_CREATE) void create() { start(url, TRANSTYPE_TCP, 0, Client.EASY_SDK_VIDEO_FRAME_FLAG | Client.EASY_SDK_AUDIO_FRAME_FLAG, "", "", null); } }); } } /** * 启动播放 * * @param url * @param type * @param sendOption * @param mediaType * @param user * @param pwd * @return */ public int start(final String url, int type, int sendOption, int mediaType, String user, String pwd) { return start(url, type, sendOption, mediaType, user, pwd, null); } /** * 启动播放 * * @param url * @param type * @param sendOption * @param mediaType * @param user * @param pwd * @return */ public int start(final String url, int type, int sendOption, int mediaType, String user, String pwd, String recordPath) { if (url == null) { throw new NullPointerException("url is null"); } if (type == 0) type = TRANSTYPE_TCP; mNewestStample = 0; mWaitingKeyFrame = PreferenceManager.getDefaultSharedPreferences(mContext).getBoolean("waiting_i_frame", true); mWidth = mHeight = 0; mQueue.clear(); startCodec(); startAudio(); mTimeout = false; mNotSupportedVideoCB = mNotSupportedAudioCB = false; mReceivedDataLength = 0; mClient = new Client(mContext); int channel = mClient.registerCallback(this); mRecordingPath = recordPath; Log.i(TAG, String.format("playing url:\n%s\n", url)); return mClient.openStream(channel, url, type, sendOption, mediaType, user, pwd); } public boolean isAudioEnable() { return mAudioEnable; } public void setAudioEnable(boolean enable) { mAudioEnable = enable; AudioTrack at = mAudioTrack; if (at != null) { Log.i(TAG, String.format("audio will be %s", enable ? "enabled" : "disabled")); synchronized (at) { if (!enable) { at.pause(); at.flush(); } else { at.flush(); at.play(); } } } } public static interface I420DataCallback { public void onI420Data(ByteBuffer buffer); } public void pause() { mQueue.clear(); if (mClient != null) { mClient.pause(); } mQueue.clear(); } public void resume() { if (mClient != null) { mClient.resume(); } } /** * 终止播放 */ public void stop() { Thread t = mThread; mThread = null; if (t != null) { t.interrupt(); try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } t = mAudioThread; mAudioThread = null; if (t != null) { t.interrupt(); try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } stopRecord(); mQueue.clear(); if (mClient != null) { mClient.unrigisterCallback(this); mClient.closeStream(); try { mClient.close(); } catch (IOException e) { e.printStackTrace(); } } mQueue.clear(); mClient = null; mNewestStample = 0; } public long receivedDataLength() { return mReceivedDataLength; } private void startAudio() { mAudioThread = new Thread("AUDIO_CONSUMER") { @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void run() { { Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO); Client.FrameInfo frameInfo; long handle = 0; final AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); AudioManager.OnAudioFocusChangeListener l = new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(int focusChange) { if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { AudioTrack audioTrack = mAudioTrack; if (audioTrack != null) { audioTrack.setStereoVolume(1.0f, 1.0f); if (audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PAUSED) { audioTrack.flush(); audioTrack.play(); } } } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { AudioTrack audioTrack = mAudioTrack; if (audioTrack != null) { if (audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { audioTrack.pause(); } } } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { AudioTrack audioTrack = mAudioTrack; if (audioTrack != null) { audioTrack.setStereoVolume(0.5f, 0.5f); } } } }; try { int requestCode = am.requestAudioFocus(l, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (requestCode != AUDIOFOCUS_REQUEST_GRANTED) { return; } do { frameInfo = mQueue.takeAudioFrame(); if (mMediaInfo != null) break; } while (true); final Thread t = Thread.currentThread(); if (mAudioTrack == null) { int sampleRateInHz = (int) (mMediaInfo.sample * 1.001); int channelConfig = mMediaInfo.channel == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO; int audioFormat = AudioFormat.ENCODING_PCM_16BIT; int bfSize = AudioTrack.getMinBufferSize(mMediaInfo.sample, channelConfig, audioFormat) * 8; mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig, audioFormat, bfSize, AudioTrack.MODE_STREAM); } mAudioTrack.play(); handle = AudioCodec.create(frameInfo.codec, frameInfo.sample_rate, frameInfo.channels, frameInfo.bits_per_sample); Log.w(TAG, String.format("POST VIDEO_DISPLAYED IN AUDIO THREAD!!!")); ResultReceiver rr = mRR; if (rr != null) rr.send(RESULT_VIDEO_DISPLAYED, null); // 半秒钟的数据缓存 byte[] mBufferReuse = new byte[16000]; int[] outLen = new int[1]; while (mAudioThread != null) { if (frameInfo == null) { frameInfo = mQueue.takeAudioFrame(); } if (frameInfo.codec == EASY_SDK_AUDIO_CODEC_AAC && false) { pumpAACSample(frameInfo); } outLen[0] = mBufferReuse.length; long ms = SystemClock.currentThreadTimeMillis(); int nRet = AudioCodec.decode(handle, frameInfo.buffer, 0, frameInfo.length, mBufferReuse, outLen); if (nRet == 0) { // if (frameInfo.codec != EASY_SDK_AUDIO_CODEC_AAC ) { // save2path(mBufferReuse, 0, outLen[0],"/sdcard/111.pcm", true); pumpPCMSample(mBufferReuse, outLen[0], frameInfo.stamp); } if (mAudioEnable) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mAudioTrack.write(mBufferReuse, 0, outLen[0], AudioTrack.WRITE_NON_BLOCKING); } else { mAudioTrack.write(mBufferReuse, 0, outLen[0]); } } frameInfo = null; } } catch (Exception ex) { ex.printStackTrace(); } finally { am.abandonAudioFocus(l); if (handle != 0) { AudioCodec.close(handle); } AudioTrack track = mAudioTrack; if (track != null) { synchronized (track) { mAudioTrack = null; track.release(); } } } } } }; mAudioThread.start(); } private static void save2path(byte[] buffer, int offset, int length, String path, boolean append) { FileOutputStream fos = null; try { fos = new FileOutputStream(path, append); fos.write(buffer, offset, length); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } } private static int getXPS(byte[] data, int offset, int length, byte[] dataOut, int[] outLen, int type) { int i; int pos0; int pos1; pos0 = -1; length = Math.min(length, data.length); for (i = offset; i < length - 4; i++) { if ((0 == data[i]) && (0 == data[i + 1]) && (1 == data[i + 2]) && (type == (0x0F & data[i + 3]))) { pos0 = i; break; } } if (-1 == pos0) { return -1; } if (pos0 > 0 && data[pos0 - 1] == 0) { // 0 0 0 1 pos0 = pos0 - 1; } pos1 = -1; for (i = pos0 + 4; i < length - 4; i++) { if ((0 == data[i]) && (0 == data[i + 1]) && (1 == data[i + 2])) { pos1 = i; break; } } if (-1 == pos1 || pos1 == 0) { return -2; } if (data[pos1 - 1] == 0) { pos1 -= 1; } if (pos1 - pos0 > outLen[0]) { return -3; // 输入缓冲区太小 } dataOut[0] = 0; System.arraycopy(data, pos0, dataOut, 0, pos1 - pos0); // memcpy(pXPS+1, pES+pos0, pos1-pos0); // *pMaxXPSLen = pos1-pos0+1; outLen[0] = pos1 - pos0; return pos1; } private static byte[] getvps_sps_pps(byte[] data, int offset, int length) { int i = 0; int vps = -1, sps = -1, pps = -1; length = Math.min(length, data.length); do { if (vps == -1) { for (i = offset; i < length - 4; i++) { if ((0x00 == data[i]) && (0x00 == data[i + 1]) && (0x01 == data[i + 2])) { byte nal_spec = data[i + 3]; int nal_type = (nal_spec >> 1) & 0x03f; if (nal_type == NAL_VPS) { // vps found. if (data[i - 1] == 0x00) { // start with 00 00 00 01 vps = i - 1; } else { // start with 00 00 01 vps = i; } break; } } } } if (sps == -1) { for (i = vps; i < length - 4; i++) { if ((0x00 == data[i]) && (0x00 == data[i + 1]) && (0x01 == data[i + 2])) { byte nal_spec = data[i + 3]; int nal_type = (nal_spec >> 1) & 0x03f; if (nal_type == NAL_SPS) { // vps found. if (data[i - 1] == 0x00) { // start with 00 00 00 01 sps = i - 1; } else { // start with 00 00 01 sps = i; } break; } } } } if (pps == -1) { for (i = sps; i < length - 4; i++) { if ((0x00 == data[i]) && (0x00 == data[i + 1]) && (0x01 == data[i + 2])) { byte nal_spec = data[i + 3]; int nal_type = (nal_spec >> 1) & 0x03f; if (nal_type == NAL_PPS) { // vps found. if (data[i - 1] == 0x00) { // start with 00 00 00 01 pps = i - 1; } else { // start with 00 00 01 pps = i; } break; } } } } } while (vps == -1 || sps == -1 || pps == -1); if (vps == -1 || sps == -1 || pps == -1) {// 没有获取成功。 return null; } // 计算csd buffer的长度。即从vps的开始到pps的结束的一段数据 int begin = vps; int end = -1; for (i = pps + 4; i < length - 4; i++) { if ((0x00 == data[i]) && (0x00 == data[i + 1]) && (0x01 == data[i + 2])) { if (data[i - 1] == 0x00) { // start with 00 00 00 01 end = i - 1; } else { // start with 00 00 01 end = i; } break; } } if (end == -1 || end < begin) { return null; } // 拷贝并返回 byte[] buf = new byte[end - begin]; System.arraycopy(data, begin, buf, 0, buf.length); return buf; } private static boolean codecMatch(String mimeType, MediaCodecInfo codecInfo) { String[] types = codecInfo.getSupportedTypes(); for (String type : types) { if (type.equalsIgnoreCase(mimeType)) { return true; } } return false; } private static String codecName() { ArrayList array = new ArrayList<>(); int numCodecs = MediaCodecList.getCodecCount(); for (int i1 = 0; i1 < numCodecs; i1++) { MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i1); if (codecInfo.isEncoder()) { continue; } if (codecMatch("video/avc", codecInfo)) { String name = codecInfo.getName(); Log.d("DECODER", String.format("decoder:%s", name)); array.add(name); } } // if (array.remove("OMX.qcom.video.decoder.avc")) { // array.add("OMX.qcom.video.decoder.avc"); // } // if (array.remove("OMX.amlogic.avc.decoder.awesome")) { // array.add("OMX.amlogic.avc.decoder.awesome"); // } if (array.isEmpty()) { return ""; } return array.get(0); } private static MediaCodecInfo selectCodec(String mimeType) { int numCodecs = MediaCodecList.getCodecCount(); for (int i = 0; i < numCodecs; i++) { MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); if (codecInfo.isEncoder()) { continue; } String[] types = codecInfo.getSupportedTypes(); for (int j = 0; j < types.length; j++) { if (types[j].equalsIgnoreCase(mimeType)) { return codecInfo; } } } return null; } private void startCodec() { mThread = new Thread("VIDEO_CONSUMER") { @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO); MediaCodec mCodec = null; int mColorFormat = 0; VideoCodec.VideoDecoderLite mDecoder = null, displayer = null; try { boolean pushBlankBuffersOnStop = true; int index = 0; // previous long previousStampUs = 0l; long lastFrameStampUs = 0l; long differ = 0; int realWidth = mWidth; int realHeight = mHeight; int sliceHeight = realHeight; int frameWidth = 0; int frameHeight = 0; // // long decodeBegin = 0; // long current = 0; MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); Client.FrameInfo initFrameInfo = null; Client.FrameInfo frameInfo = null; while (mThread != null) { if (mCodec == null && mDecoder == null) { if (frameInfo == null) { frameInfo = mQueue.takeVideoFrame(); } initFrameInfo = frameInfo; try { if (PreferenceManager.getDefaultSharedPreferences(mContext).getBoolean("use-sw-codec", false)) { throw new IllegalStateException("user set sw codec"); } final String mime = frameInfo.codec == EASY_SDK_VIDEO_CODEC_H264 ? "video/avc" : "video/hevc"; MediaFormat format = MediaFormat.createVideoFormat(mime, mWidth, mHeight); format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0); format.setInteger(MediaFormat.KEY_PUSH_BLANK_BUFFERS_ON_STOP, pushBlankBuffersOnStop ? 1 : 0); // 指定解码后的帧格式 format.setInteger(MediaFormat.KEY_COLOR_FORMAT, COLOR_FormatYUV420Flexible); if (mCSD0 != null) { format.setByteBuffer("csd-0", mCSD0); } else { throw new InvalidParameterException("csd-0 is invalid."); } if (mCSD1 != null) { format.setByteBuffer("csd-1", mCSD1); } else { if (frameInfo.codec == EASY_SDK_VIDEO_CODEC_H264) throw new InvalidParameterException("csd-1 is invalid."); } MediaCodecInfo ci = selectCodec(mime); mColorFormat = CodecSpecificDataUtil.selectColorFormat(ci, mime); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { MediaCodecInfo.CodecCapabilities capabilities = ci.getCapabilitiesForType(mime); MediaCodecInfo.VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); boolean supported = videoCapabilities.isSizeSupported(mWidth, mHeight); Log.i(TAG, "media codec " + ci.getName() + (supported ? "support" : "not support") + mWidth + "*" + mHeight); if (!supported) { boolean b1 = videoCapabilities.getSupportedWidths().contains(mWidth + 0); boolean b2 = videoCapabilities.getSupportedHeights().contains(mHeight + 0); supported |= b1 && b2; if (supported) { Log.w(TAG, "......................................................................."); } else { throw new IllegalStateException("media codec " + ci.getName() + (supported ? "support" : "not support") + mWidth + "*" + mHeight); } } } Log.i(TAG, String.format("config codec:%s", format)); MediaCodec codec = MediaCodec.createByCodecName(ci.getName()); codec.configure(format, mSurface, null, 0); codec.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); codec.start(); mCodec = codec; // if (i420callback != null) { // final VideoCodec.VideoDecoderLite decoder = new VideoCodec.VideoDecoderLite(); // decoder.create(mSurface, frameInfo.codec == EASY_SDK_VIDEO_CODEC_H264); // displayer = decoder; // } } catch (Throwable e) { if (mCodec != null) { mCodec.release(); } mCodec = null; if (displayer != null) { displayer.close(); Log.i(TAG, "AAAA 958 displayer.close()"); } displayer = null; Log.e(TAG, String.format("init codec error due to %s", e.getMessage())); e.printStackTrace(); final VideoCodec.VideoDecoderLite decoder = new VideoCodec.VideoDecoderLite(); decoder.create(mSurface, frameInfo.codec == EASY_SDK_VIDEO_CODEC_H264); mDecoder = decoder; } // previewTickUs = mTexture.getTimestamp(); // differ = previewTickUs - frameInfo.stamp; // index = mCodec.dequeueInputBuffer(0); // if (index >= 0) { // ByteBuffer buffer = mCodec.getInputBuffers()[index]; // buffer.clear(); // mCSD0.clear(); // mCSD1.clear(); // buffer.put(mCSD0.array(), 0, mCSD0.remaining()); // buffer.put(mCSD1.array(), 0, mCSD1.remaining()); // mCodec.queueInputBuffer(index, 0, buffer.position(), 0, MediaCodec.BUFFER_FLAG_CODEC_CONFIG); // } } else { frameInfo = mQueue.takeVideoFrame(5); } if (frameInfo != null) { Log.d(TAG, "video " + frameInfo.stamp + " take[" + (frameInfo.stamp - lastFrameStampUs) + "]"); if (frameHeight != 0 && frameWidth != 0) { if (frameInfo.width != 0 && frameInfo.height != 0) { if (frameInfo.width != frameWidth || frameInfo.height != frameHeight) { frameHeight = frameInfo.height; frameWidth = frameInfo.width; stopRecord(); if (mCodec != null) { mCodec.release(); mCodec = null; continue; } } } } frameHeight = frameInfo.height; frameWidth = frameInfo.width; pumpVideoSample(frameInfo); lastFrameStampUs = frameInfo.stamp; } do { if (mDecoder != null) { if (frameInfo != null) { long decodeBegin = SystemClock.elapsedRealtime(); int[] size = new int[2]; // mDecoder.decodeFrame(frameInfo, size); ByteBuffer buf = mDecoder.decodeFrameYUV(frameInfo, size); if (i420callback != null && buf != null) { i420callback.onI420Data(buf); } if (buf != null) { mDecoder.releaseBuffer(buf); Log.i(TAG, "AAAA 1022 releaseBuffer "); } long decodeSpend = SystemClock.elapsedRealtime() - decodeBegin; boolean firstFrame = previousStampUs == 0l; if (firstFrame) { Log.i(TAG, String.format("POST VIDEO_DISPLAYED!!!")); ResultReceiver rr = mRR; if (rr != null) { Bundle data = new Bundle(); data.putInt(KEY_VIDEO_DECODE_TYPE, 0); rr.send(RESULT_VIDEO_DISPLAYED, data); } } //Log.d(TAG, String.format("timestamp=%d diff=%d",current, current - previousStampUs )); if (previousStampUs != 0l) { long sleepTime = frameInfo.stamp - previousStampUs - decodeSpend * 1000; if (sleepTime > 100000) { Log.w(TAG, "sleep time.too long:" + sleepTime); sleepTime = 100000; } if (sleepTime > 0) { sleepTime %= 100000; long cache = mNewestStample - frameInfo.stamp; sleepTime = fixSleepTime(sleepTime, cache, 50000); if (sleepTime > 0) { Thread.sleep(sleepTime / 1000); } Log.d(TAG, "cache:" + cache); } } previousStampUs = frameInfo.stamp; } } else { try { do { if (frameInfo != null) { byte[] pBuf = frameInfo.buffer; index = mCodec.dequeueInputBuffer(10); if (false) throw new IllegalStateException("fake state"); if (index >= 0) { ByteBuffer buffer = mCodec.getInputBuffers()[index]; buffer.clear(); if (pBuf.length > buffer.remaining()) { mCodec.queueInputBuffer(index, 0, 0, frameInfo.stamp, 0); } else { buffer.put(pBuf, frameInfo.offset, frameInfo.length); mCodec.queueInputBuffer(index, 0, buffer.position(), frameInfo.stamp + differ, 0); } frameInfo = null; } } index = mCodec.dequeueOutputBuffer(info, 10); // switch (index) { case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: Log.i(TAG, "INFO_OUTPUT_BUFFERS_CHANGED"); break; case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: MediaFormat mf = mCodec.getOutputFormat(); Log.i(TAG, "INFO_OUTPUT_FORMAT_CHANGED :" + mf); int width = mf.getInteger(MediaFormat.KEY_WIDTH); if (mf.containsKey("crop-left") && mf.containsKey("crop-right")) { width = mf.getInteger("crop-right") + 1 - mf.getInteger("crop-left"); } int height = mf.getInteger(MediaFormat.KEY_HEIGHT); if (mf.containsKey("crop-top") && mf.containsKey("crop-bottom")) { height = mf.getInteger("crop-bottom") + 1 - mf.getInteger("crop-top"); } realWidth = width; realHeight = height; if (mf.containsKey(MediaFormat.KEY_SLICE_HEIGHT)) { sliceHeight = mf.getInteger(MediaFormat.KEY_SLICE_HEIGHT); } else { sliceHeight = realHeight; } break; case MediaCodec.INFO_TRY_AGAIN_LATER: // 输出为空 break; default: // 输出队列不为空 // -1表示为第一帧数据 long newSleepUs = -1; boolean firstTime = previousStampUs == 0l; if (!firstTime) { long sleepUs = (info.presentationTimeUs - previousStampUs); if (sleepUs > 100000) { // 时间戳异常,可能服务器丢帧了。 Log.w(TAG, "sleep time.too long:" + sleepUs); sleepUs = 100000; } else if (sleepUs < 0) { Log.w(TAG, "sleep time.too short:" + sleepUs); sleepUs = 0; } { long cache = mNewestStample - lastFrameStampUs; newSleepUs = fixSleepTime(sleepUs, cache, 100000); // Log.d(TAG, String.format("sleepUs:%d,newSleepUs:%d,Cache:%d", sleepUs, newSleepUs, cache)); } } //previousStampUs = info.presentationTimeUs; ByteBuffer outputBuffer; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { outputBuffer = mCodec.getOutputBuffer(index); } else { outputBuffer = mCodec.getOutputBuffers()[index]; } if (i420callback != null && outputBuffer != null) { // if (sliceHeight != realHeight) { // ByteBuffer tmp = ByteBuffer.allocateDirect(realWidth * realHeight * 3 / 2); // outputBuffer.clear(); // outputBuffer.limit(realWidth * realHeight); // tmp.put(outputBuffer); // // outputBuffer.clear(); // outputBuffer.position(realWidth * sliceHeight); // outputBuffer.limit((realWidth * sliceHeight + realWidth * realHeight / 4)); // tmp.put(outputBuffer); // // outputBuffer.clear(); // outputBuffer.position(realWidth * sliceHeight + realWidth * realHeight / 4); // outputBuffer.limit((realWidth * sliceHeight + realWidth * realHeight / 4 + realWidth * realHeight / 4)); // tmp.put(outputBuffer); // // tmp.clear(); // outputBuffer = tmp; // } if (mColorFormat == COLOR_FormatYUV420SemiPlanar || mColorFormat == COLOR_FormatYUV420PackedSemiPlanar || mColorFormat == COLOR_TI_FormatYUV420PackedSemiPlanar) { // byte[] in = new byte[realWidth * realHeight * 3 / 2]; // outputBuffer.clear(); // outputBuffer.get(in); // // // yuvuv_to_yuv // JNIUtil.yuvConvert(in, realWidth, realHeight, 4); //// // 旋转90或180或270度 //// yuvRotate(in, 0, realWidth, realHeight, 90); // // ByteBuffer tmp = ByteBuffer.allocateDirect(realWidth * realHeight * 3 / 2); // tmp.clear(); // tmp.put(in); i420callback.onI420Data(null); // // 旋转90或270度,则宽高需要互换 // displayer.decoder_decodeBuffer(tmp, realWidth, realHeight); } } //previewStampUs = info.presentationTimeUs; if (false && Build.VERSION.SDK_INT >= 21) { Log.d(TAG, String.format("releaseoutputbuffer:%d,stampUs:%d", index, previousStampUs)); mCodec.releaseOutputBuffer(index, previousStampUs); } else { if (newSleepUs < 0) { newSleepUs = 0; } // Log.d(TAG,String.format("sleep:%d", newSleepUs/1000)); Thread.sleep(newSleepUs / 1000); mCodec.releaseOutputBuffer(index, true); } if (firstTime) { Log.i(TAG, String.format("POST VIDEO_DISPLAYED!!!")); ResultReceiver rr = mRR; if (rr != null) { Bundle data = new Bundle(); data.putInt(KEY_VIDEO_DECODE_TYPE, 1); rr.send(RESULT_VIDEO_DISPLAYED, data); } } previousStampUs = info.presentationTimeUs; } } while (frameInfo != null || index < MediaCodec.INFO_TRY_AGAIN_LATER); } catch (IllegalStateException ex) { // mediacodec error... ex.printStackTrace(); Log.e(TAG, String.format("init codec error due to %s", ex.getMessage())); if (mCodec != null) mCodec.release(); mCodec = null; if (displayer != null) { displayer.close(); Log.i(TAG, "AAAA 1217 displayer.close()"); } displayer = null; final VideoCodec.VideoDecoderLite decoder = new VideoCodec.VideoDecoderLite(); decoder.create(mSurface, initFrameInfo.codec == EASY_SDK_VIDEO_CODEC_H264); mDecoder = decoder; continue; } } break; } while (true); } } catch (Exception e) { e.printStackTrace(); } finally { if (mCodec != null) { // mCodec.stop(); mCodec.release(); } if (mDecoder != null) { mDecoder.close(); Log.i(TAG, "AAAA 1238 mDecoder.close();"); } if (displayer != null) { displayer.close(); Log.i(TAG, "AAAA 1243 displayer.close();"); } } } }; mThread.start(); } private static final long fixSleepTime(long sleepTimeUs, long totalTimestampDifferUs, long delayUs) { if (totalTimestampDifferUs < 0l) { Log.w(TAG, String.format("totalTimestampDifferUs is:%d, this should not be happen.", totalTimestampDifferUs)); totalTimestampDifferUs = 0; } double dValue = ((double) (delayUs - totalTimestampDifferUs)) / 1000000d; double radio = Math.exp(dValue); double r = sleepTimeUs * radio + 0.5f; Log.i(TAG, String.format("%d,%d,%d->%d", sleepTimeUs, totalTimestampDifferUs, delayUs, (int) r)); return (long) r; } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) public synchronized void startRecord(String path) { if (mMediaInfo == null || mWidth == 0 || mHeight == 0 || mCSD0 == null) return; mRecordingPath = path; EasyMuxer2 muxer2 = new EasyMuxer2(); mMuxerCuttingMillis = 0l; mRecordingStatus = 0; muxerPausedMillis = 0; ByteBuffer csd1 = this.mCSD1; if (csd1 == null) csd1 = ByteBuffer.allocate(0); byte[] extra = new byte[mCSD0.capacity() + csd1.capacity()]; mCSD0.clear(); csd1.clear(); mCSD0.get(extra, 0, mCSD0.capacity()); csd1.get(extra, mCSD0.capacity(), csd1.capacity()); int r = muxer2.create(path, mMediaInfo.videoCodec == EASY_SDK_VIDEO_CODEC_H265 ? VIDEO_TYPE_H265 : VIDEO_TYPE_H264, mWidth, mHeight, extra, mMediaInfo.sample, mMediaInfo.channel); if (r != 0) { Log.w(TAG, "create muxer2:" + r); return; } mMuxerWaitingKeyVideo = true; this.muxer2 = muxer2; ResultReceiver rr = mRR; if (rr != null) { rr.send(RESULT_RECORD_BEGIN, null); } } public synchronized void pauseRecord() { if (mRecordingStatus != -1) { mRecordingStatus = -1; muxerPausedMillis = SystemClock.elapsedRealtime(); } } public synchronized void resumeRecord() { if (mRecordingStatus == -1) { mMuxerWaitingKeyVideo = true; mRecordingStatus = 1; } } private static int getSampleIndex(int sample) { for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE.length; i++) { if (sample == AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[i]) { return i; } } return -1; } private void pumpAACSample(Client.FrameInfo frameInfo) { EasyMuxer muxer = mObject; if (muxer == null) return; MediaCodec.BufferInfo bi = new MediaCodec.BufferInfo(); bi.offset = frameInfo.offset; bi.size = frameInfo.length; ByteBuffer buffer = ByteBuffer.wrap(frameInfo.buffer, bi.offset, bi.size); bi.presentationTimeUs = frameInfo.stamp; try { if (!frameInfo.audio) { throw new IllegalArgumentException("frame should be audio!"); } if (frameInfo.codec != EASY_SDK_AUDIO_CODEC_AAC) { throw new IllegalArgumentException("audio codec should be aac!"); } bi.offset += 7; bi.size -= 7; muxer.pumpStream(buffer, bi, false); } catch (IllegalStateException ex) { ex.printStackTrace(); } } private synchronized void pumpPCMSample(byte[] pcm, int length, long stampUS) { EasyMuxer2 muxer2 = this.muxer2; if (muxer2 == null) return; if (mRecordingStatus < 0) return; if (mMuxerWaitingKeyVideo) { Log.i(TAG, "writeFrame ignore due to no key frame!"); return; } long timeStampMillis = stampUS / 1000; timeStampMillis -= mMuxerCuttingMillis; timeStampMillis = Math.max(0, timeStampMillis); int r = muxer2.writeFrame(EasyMuxer2.AVMEDIA_TYPE_AUDIO, pcm, 0, length, timeStampMillis); Log.i(TAG, "writeFrame audio ret:" + r); } private synchronized void pumpVideoSample(Client.FrameInfo frameInfo) { EasyMuxer2 muxer2 = this.muxer2; if (muxer2 == null) return; if (mRecordingStatus < 0) return; if (mMuxerWaitingKeyVideo) { if (frameInfo.type == 1) { mMuxerWaitingKeyVideo = false; if (mRecordingStatus == 1) { mMuxerCuttingMillis += SystemClock.elapsedRealtime() - muxerPausedMillis; mRecordingStatus = 0; } } } if (mMuxerWaitingKeyVideo) { Log.i(TAG, "writeFrame ignore due to no key frame!"); return; } if (frameInfo.type == 1) { // frameInfo.offset = 60; // frameInfo.length -= 60; } long timeStampMillis = frameInfo.stamp / 1000; timeStampMillis -= mMuxerCuttingMillis; timeStampMillis = Math.max(0, timeStampMillis); int r = muxer2.writeFrame(EasyMuxer2.AVMEDIA_TYPE_VIDEO, frameInfo.buffer, frameInfo.offset, frameInfo.length, timeStampMillis); Log.i(TAG, "writeFrame video ret:" + r); } public synchronized void stopRecord() { mRecordingPath = null; mMuxerCuttingMillis = 0l; mRecordingStatus = 0; muxerPausedMillis = 0; EasyMuxer2 muxer2 = this.muxer2; if (muxer2 == null) return; this.muxer2 = null; muxer2.close(); mObject = null; ResultReceiver rr = mRR; if (rr != null) { rr.send(RESULT_RECORD_END, null); } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void onSourceCallBack(int _channelId, int _channelPtr, int _frameType, Client.FrameInfo frameInfo) { // long begin = SystemClock.elapsedRealtime(); try { onRTSPSourceCallBack1(_channelId, _channelPtr, _frameType, frameInfo); } catch (Throwable e) { e.printStackTrace(); } finally { // Log.d(TAG, String.format("onRTSPSourceCallBack %d", SystemClock.elapsedRealtime() - begin)); } } public void onRTSPSourceCallBack1(int _channelId, int _channelPtr, int _frameType, Client.FrameInfo frameInfo) { Thread.currentThread().setName("PRODUCER_THREAD"); if (frameInfo != null) { mReceivedDataLength += frameInfo.length; } if (_frameType == Client.EASY_SDK_VIDEO_FRAME_FLAG) { //Log.d(TAG,String.format("receive video frame")); if (frameInfo.codec != EASY_SDK_VIDEO_CODEC_H264 && frameInfo.codec != EASY_SDK_VIDEO_CODEC_H265) { ResultReceiver rr = mRR; if (!mNotSupportedVideoCB && rr != null) { mNotSupportedVideoCB = true; rr.send(RESULT_UNSUPPORTED_VIDEO, null); } return; } // save2path(frameInfo.buffer, 0, frameInfo.length, "/sdcard/264.h264", true); if (frameInfo.width == 0 || frameInfo.height == 0) { return; } if (frameInfo.length >= 4) { if (frameInfo.buffer[0] == 0 && frameInfo.buffer[1] == 0 && frameInfo.buffer[2] == 0 && frameInfo.buffer[3] == 1) { if (frameInfo.length >= 8) { if (frameInfo.buffer[4] == 0 && frameInfo.buffer[5] == 0 && frameInfo.buffer[6] == 0 && frameInfo.buffer[7] == 1) { frameInfo.offset += 4; frameInfo.length -= 4; } } } } // int offset = frameInfo.offset; // byte nal_unit_type = (byte) (frameInfo.buffer[offset + 4] & (byte) 0x1F); // if (nal_unit_type == 7 || nal_unit_type == 5) { // Log.i(TAG,String.format("recv I frame")); // } if (frameInfo.type == 1) { Log.i(TAG, String.format("recv I frame")); } // boolean firstFrame = mNewestStample == 0; mNewestStample = frameInfo.stamp; frameInfo.audio = false; if (mWaitingKeyFrame) { ResultReceiver rr = mRR; Bundle bundle = new Bundle(); bundle.putInt(EXTRA_VIDEO_WIDTH, frameInfo.width); bundle.putInt(EXTRA_VIDEO_HEIGHT, frameInfo.height); mWidth = frameInfo.width; mHeight = frameInfo.height; Log.i(TAG, String.format("RESULT_VIDEO_SIZE:%d*%d", frameInfo.width, frameInfo.height)); if (rr != null) rr.send(RESULT_VIDEO_SIZE, bundle); Log.i(TAG, String.format("width:%d,height:%d", mWidth, mHeight)); if (frameInfo.codec == EASY_SDK_VIDEO_CODEC_H264) { byte[] dataOut = new byte[128]; int[] outLen = new int[]{128}; int result = getXPS(frameInfo.buffer, 0, 256, dataOut, outLen, 7); if (result >= 0) { ByteBuffer csd0 = ByteBuffer.allocate(outLen[0]); csd0.put(dataOut, 0, outLen[0]); csd0.clear(); mCSD0 = csd0; Log.i(TAG, String.format("CSD-0 searched")); } outLen[0] = 128; result = getXPS(frameInfo.buffer, 0, 256, dataOut, outLen, 8); if (result >= 0) { ByteBuffer csd1 = ByteBuffer.allocate(outLen[0]); csd1.put(dataOut, 0, outLen[0]); csd1.clear(); mCSD1 = csd1; Log.i(TAG, String.format("CSD-1 searched")); } if (false) { int off = (result - frameInfo.offset); frameInfo.offset += off; frameInfo.length -= off; } } else { byte[] spsPps = getvps_sps_pps(frameInfo.buffer, 0, 256); if (spsPps != null) { mCSD0 = ByteBuffer.wrap(spsPps); } } if (frameInfo.type != 1) { Log.w(TAG, String.format("discard p frame.")); return; } mWaitingKeyFrame = false; synchronized (this) { if (!TextUtils.isEmpty(mRecordingPath) && mObject == null) { startRecord(mRecordingPath); } } } else { int width = frameInfo.width; int height = frameInfo.height; if (width != 0 && height != 0) if (width != mWidth || height != mHeight) { // resolution change... ResultReceiver rr = mRR; Bundle bundle = new Bundle(); bundle.putInt(EXTRA_VIDEO_WIDTH, frameInfo.width); bundle.putInt(EXTRA_VIDEO_HEIGHT, frameInfo.height); mWidth = frameInfo.width; mHeight = frameInfo.height; Log.i(TAG, String.format("RESULT_VIDEO_SIZE:%d*%d", frameInfo.width, frameInfo.height)); if (rr != null) rr.send(RESULT_VIDEO_SIZE, bundle); } } // Log.d(TAG, String.format("queue size :%d", mQueue.size())); try { mQueue.put(frameInfo); } catch (InterruptedException e) { e.printStackTrace(); } } else if (_frameType == Client.EASY_SDK_AUDIO_FRAME_FLAG) { mNewestStample = frameInfo.stamp; frameInfo.audio = true; if (true) { if (frameInfo.codec != EASY_SDK_AUDIO_CODEC_AAC && frameInfo.codec != EASY_SDK_AUDIO_CODEC_G711A && frameInfo.codec != EASY_SDK_AUDIO_CODEC_G711U && frameInfo.codec != EASY_SDK_AUDIO_CODEC_G726) { ResultReceiver rr = mRR; if (!mNotSupportedAudioCB && rr != null) { mNotSupportedAudioCB = true; if (rr != null) { rr.send(RESULT_UNSUPPORTED_AUDIO, null); } } return; } } Log.d(TAG, String.format("queue size :%d", mQueue.size())); try { mQueue.put(frameInfo); } catch (InterruptedException e) { e.printStackTrace(); } } else if (_frameType == 0) { // time out... if (!mTimeout) { mTimeout = true; ResultReceiver rr = mRR; if (rr != null) rr.send(RESULT_TIMEOUT, null); } } else if (_frameType == Client.EASY_SDK_EVENT_FRAME_FLAG) { ResultReceiver rr = mRR; Bundle resultData = new Bundle(); resultData.putString("event-msg", new String(frameInfo.buffer)); if (rr != null) rr.send(RESULT_EVENT, null); } } @Override public void onMediaInfoCallBack(int _channelId, Client.MediaInfo mi) { mMediaInfo = mi; Log.i(TAG, String.format("MediaInfo fetchd\n%s", mi)); } @Override public void onEvent(int channel, int err, int info) { ResultReceiver rr = mRR; Bundle resultData = new Bundle(); /* int state = 0; int err = EasyRTSP_GetErrCode(fRTSPHandle); // EasyRTSPClient开始进行连接,建立EasyRTSPClient连接线程 if (NULL == _pBuf && NULL == _frameInfo) { LOGD("Recv Event: Connecting..."); state = 1; } // EasyPlayerClient RTSPClient连接错误,错误码通过EasyRTSP_GetErrCode()接口获取,比如404 else if (NULL != _frameInfo && _frameInfo->codec == EASY_SDK_EVENT_CODEC_ERROR) { LOGD("Recv Event: Error:%d ...\n", err); state = 2; } // EasyRTSPClient连接线程退出,此时上层应该停止相关调用,复位连接按钮等状态 else if (NULL != _frameInfo && _frameInfo->codec == EASY_SDK_EVENT_CODEC_EXIT) { LOGD("Recv Event: Exit,Error:%d ...", err); state = 3; } * */ switch (info) { case 1: resultData.putString("event-msg", "连接中..."); break; case 2: resultData.putInt("errorcode", err); resultData.putString("event-msg", String.format("错误:%d", err)); break; case 3: resultData.putInt("errorcode", err); resultData.putString("event-msg", String.format("线程退出。%d", err)); break; } if (rr != null) rr.send(RESULT_EVENT, resultData); } /** * 旋转YUV格式数据 * * @param src YUV数据 * @param format 0,420P;1,420SP * @param width 宽度 * @param height 高度 * @param degree 旋转度数 */ private static void yuvRotate(byte[] src, int format, int width, int height, int degree) { int offset = 0; if (format == 0) { JNIUtil.rotateMatrix(src, offset, width, height, degree); offset += (width * height); JNIUtil.rotateMatrix(src, offset, width / 2, height / 2, degree); offset += width * height / 4; JNIUtil.rotateMatrix(src, offset, width / 2, height / 2, degree); } else if (format == 1) { JNIUtil.rotateMatrix(src, offset, width, height, degree); offset += width * height; JNIUtil.rotateShortMatrix(src, offset, width / 2, height / 2, degree); } } }