package com.flazr.rtmp.reader;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import nliveroid.nlr.main.BCPlayer;
import nliveroid.nlr.main.LiveSettings;
import nliveroid.nlr.main.MyToast;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.util.internal.LinkedTransferQueue;
import android.graphics.Rect;
import android.hardware.Camera;
import android.os.AsyncTask;
import android.util.Log;
import android.view.SurfaceHolder;
import com.flazr.rtmp.RtmpHeader;
import com.flazr.rtmp.RtmpMessage;
import com.flazr.rtmp.client.CameraParams;
import com.flazr.rtmp.client.RealTimeMic;
import com.flazr.rtmp.message.MessageType;
import com.flazr.rtmp.message.MetadataAmf0;
import com.flazr.util.Utils;
public class PreviewReader implements RtmpReader , Camera.PreviewCallback{
private MetadataAmf0 metadata;
private LinkedTransferQueue<RtmpMessage> globalQueue;
private LiveSettings liveSetting;
private RealTimeMic mic;
private BCPlayer player;
private LinkedTransferQueue<byte[]> rawQueue;
private Camera mCam;
private EncodingLoop encodeTask;
private boolean ENDFLAG = true;
private boolean startedPreview;
private SurfaceHolder.Callback callBack;
private SurfaceHolder mHolder;
private boolean isInited = false;
private int offerValue = 0;
private CameraParams parameters;
private native int initCamNative(int width,int height, int i, int j, int k, int l);
private native int endCamNative();
private native void setVideoTimUnit(int timeunit);
private native int encodeYUVArray(byte[] cameraYUV420,boolean isportlayt);
private native int fileTest();
//カメラプレビューとりあえず固定取得する
public PreviewReader(BCPlayer player_,LiveSettings liveSettings) {
this.player = player_;
this.liveSetting = liveSettings;
globalQueue = new LinkedTransferQueue<RtmpMessage>();
rawQueue = new LinkedTransferQueue<byte[]>();
}
@Override
public int init(final String path) {//引数nullでくる
//カメラを起動する
try{
Log.d("NLiveRoid","Cam_init");
ENDFLAG = false;//解像度設定変更などで呼ばれる場合、エンコーダを完全に終了するべき
long timeout = System.currentTimeMillis();
if(liveSetting.isEncodeStarted()){
player.stopStream();
}
while(liveSetting.isEncodeStarted()){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
if(timeout - System.currentTimeMillis() > 60000){
return -10;
}
}
if(liveSetting.isUseCam()){
mCam = Camera.open();
if(mCam == null)return -3;
parameters = new CameraParams(player,liveSetting);
parameters.init(mCam);
//ビューの表示に必要なパラメタをセットする
}
//解像度設定後でないとMetaDataにサイズが設定できない
//FLVの最初はどっかでセットされている
final RtmpMessage metadataAtom = new PreviewMetaData();
//メッセージタイプに渡されて、メタデータがデコードされる
metadata = (MetadataAmf0) MessageType.decode(metadataAtom.getHeader(), metadataAtom.encode());
if(liveSetting == null)return -1;
isInited = true;
}catch(RuntimeException e){
e.printStackTrace();
if(startedPreview)mCam.stopPreview();
return -1;
}
return 0;
}
public int startEncode(){
if(encodeTask != null && encodeTask.getStatus() == AsyncTask.Status.RUNNING){
encodeTask.cancel(true);
}
ENDFLAG = true;
encodeTask = new EncodingLoop();
encodeTask.execute();
return 0;
}
@Override
public void onPreviewFrame(byte[] arg, Camera arg1) {
mCam.addCallbackBuffer(arg);//OOM対策
if(!liveSetting.isStreamStarted())return;
try{
Log.d("RealTimeCam","RAWSIZE --- " +rawQueue.size());
if(offerValue > 0&&rawQueue.size() < 2){//エンコードが間に合わなかった時の為にここでrawQueueサイズ制限は必要
if(arg != null)rawQueue.offer(arg);//カメラでフレームのアロケートに失敗する場合がある下ログ参照
offerValue --;
}
}catch(Exception e){
e.printStackTrace();
MyToast.customToastShow(player, "MemoryOverflow:映像送信をストップしました");
stopPreview();
}
}
class EncodingLoop extends AsyncTask<Void,Void,Integer>{
@Override
protected Integer doInBackground(Void... params) {
Log.d("RealTimeCam","StartEncodint loop");
liveSetting.setEncodeStarted(true);
//エンコーダのinitとエンコードが同一のスレッド
if(liveSetting.isUseMic()){
if(mic == null){
mic = new RealTimeMic(player);
mic.setReader(PreviewReader.this);
}
if(!mic.isInited()&&mic.init(liveSetting)<0){
ENDFLAG = false;
return -6;
}
if(!mic.isRecording()){
mic.startRecording();
}
//マイク側がスタートするまで待つ
while(ENDFLAG && mic == null){
}
while(ENDFLAG && mic.getStartsync()){
}
Log.d("PictureReader","StartMicOnPreviewReader --------------------- ");
}
if(!liveSetting.isUseCam())return 0;//マイクのみだったら何もしない
//getNowEncodeResolutionは縦の時に黒を入れることを考えたサイズ
// final Rect nowSize = liveSetting.getNowEncodeResolution();
final Rect nowSize = liveSetting.getNowActualResolution();
if(nowSize == null){
Log.d("RealTimeCam","Failed -11 loop");
liveSetting.setUseCam(false);
return -11;
}
if(initCamNative(nowSize.right,nowSize.bottom,liveSetting.getUser_fps(),liveSetting.getV_Bit_rate(),liveSetting.getKeyframe_interval(),liveSetting.isUseMic()? 1:0)<0){
liveSetting.setUseCam(false);
return -3;
}
System.gc();//ネイティブでちゃんと値がはいってない場合はGCすると落ちるのでわかる
while(!startedPreview){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Log.d("RealTimeCam","Reader null Interrupt" + ENDFLAG );
e.printStackTrace();
}
}
while(ENDFLAG&&startedPreview){
byte[] yuv = null;
long mil = System.currentTimeMillis();
long nano = System.nanoTime();
try {
yuv = rawQueue.take();//ここで突っ込まれたYUVを順に取り出してエンコーダに渡す
} catch (InterruptedException e) {
e.printStackTrace();
}
//ネイティブに渡してネイティブからsetGrobalQueueを呼んでRTMPへ
if(encodeYUVArray(yuv,true)<0){
endCamNative();
return -1;
}
Log.d("NLiveRoid","ENCODE_MILL_TIME:" + (System.currentTimeMillis()-mil));
// Log.d("NLiveRoid","ENCODE_NANO_TIME:" + (System.nanoTime()-nano));
}
endCamNative();
Log.d("RealTimeCam","END Encoding loop");
liveSetting.setEncodeStarted(false);
return 0;
}
@Override
protected void onPostExecute(Integer arg){
if(arg == -1){
MyToast.customToastShow(player, "カメラのエンコードに失敗したため停止しました");
}else if(arg == -2){
MyToast.customToastShow(player, "初期化に失敗していたためカメラを停止しました。端末の再起動で改善することがあります");
}else if(arg == -3){
MyToast.customToastShow(player, "解像度に非対応。処理を停止しました");
}
}
}
//ネイティブから呼ばれる
public void setGrobalQueue(byte[] header_,byte[] data_,int size){
Log.d("NLiveRoid","CAM -------- setGrobalQueue Called size:"+globalQueue.size());
if(globalQueue.size() > 5){
globalQueue.clear();
}else{
globalQueue.offer(new CamAtom(header_,data_,size));
}
}
public int startPreview(){
Log.d("NLiveRoid","RealTimeCam startPreview Called");
if(mCam == null)return -1;
if(startedPreview){
mCam.stopPreview();
startedPreview = false;
}
if(fpsTick == null){
fpsTick = new Timer();
fpsTick.schedule(new Tick(), 0,1000);
}
int value = resetPreviewDisplay();
if(value <0){
Log.d("NLiveRoid","Failed resetPreviewDisplay:"+value);
return -2;
}
try{
mCam.startPreview();
}catch(Exception e){
e.printStackTrace();
return -3;
}
startedPreview = true;
Log.d("RealTimeCam","StartedPreviewrCam");
return 0;
}
/**
* startedPreviewを取得します。
* @return startedPreview
*/
public boolean isStartedPreview() {
return startedPreview;
}
public CameraParams getParameters(){
return parameters;
}
private Timer fpsTick;
class Tick extends TimerTask{
private int prevFrameCount;
@Override
public void run() {
if(liveSetting.isStreamStarted()){
// realFPS = previewCount - prevFrameCount;
// //設定値でエンコードされるので、
// //実際出ているFPSより設定値が高ければ同じフレームを追加、超えてる場合は減らす
// dummyCount += (liveSetting.getUser_fps() - realFPS);
// Log.d("ReadTimeCam","FPS " + realFPS +" " + dummyCount);
// canOffer = liveSetting.getUser_fps();
offerValue = 2;
Log.d("ReadTimeCam","FPS canOffer " + offerValue);
}
}
}
public boolean isInited(){
return isInited;
}
//startPreviewは複数個所から連続されると非常に困るので、エンコードと分けたが、ストップは問題なかろう
public int stopPreview() {
try {
ENDFLAG = false;
if(encodeTask != null && (encodeTask.getStatus() == AsyncTask.Status.RUNNING||encodeTask.getStatus() == AsyncTask.Status.PENDING)){
encodeTask.cancel(true);
}
if(fpsTick != null){
fpsTick.cancel();
}
Log.d("RealTimeCam","stopPreview");
if (this.mCam != null) {
mCam.setPreviewDisplay(null);
mCam.setPreviewCallback(null);
mCam.stopPreview();
mCam.release();
}
} catch (Exception e) {
e.printStackTrace();
return -1;
}
isInited = false;
startedPreview = false;
return 0;
}
public int resetPreviewDisplay(){
try {
mCam.setPreviewDisplay(null);
mCam.setPreviewCallback(null);
mCam.setPreviewCallbackWithBuffer(null);
} catch (IOException e1) {
e1.printStackTrace();
if(startedPreview)mCam.stopPreview();
return -3;
}
mHolder = player.getCamSurfaceHolder();
Log.d("HOLDER"," " + mHolder);
if(callBack == null){
callBack = new SurfaceHolder.Callback() {
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
};
}else{
mHolder.removeCallback(callBack);
}
mHolder.addCallback(callBack);
try {
mCam.setPreviewCallbackWithBuffer(this);
Rect rect = liveSetting.getNowActualResolution();
Log.d("NLiveRoid","ADD_BUFFER_CAM "+ rect.right + " " + rect.bottom);
mCam.addCallbackBuffer(new byte[rect.right*rect.bottom*3/2]);
mCam.setPreviewDisplay(mHolder);
} catch (IOException e) {
e.printStackTrace();
if(startedPreview){
mCam.stopPreview();
startedPreview = false;
}
return -2;
}
return 0;
}
public void releaseCamera(){
ENDFLAG = false;
isInited = false;
if(mCam != null)mCam.release();
mCam = null;
if(fpsTick != null)fpsTick.cancel();
}
class CamAtom implements RtmpMessage {
private final RtmpHeader header;
private ChannelBuffer data;
//カメラプレビューのメインで呼ばれる
public CamAtom(final byte[] av_frame) {
//とりあえずここでサイズに合わせてタグのヘッダーを生成しておく
//タグタイプ1バイト、サイズ3バイト、タイムスタンプ3バイト、タイムスタンプ拡張1バイト、ストリームID3バイト
int size = av_frame.length;
byte[] headerBytes = new byte[11];
System.arraycopy(av_frame, 0, headerBytes, 0, 11);
// Log.d("CamPreviewAtom","Header "+Utils.toHex(headerBytes, 0, headerBytes.length, true));
header = readHeader(headerBytes);
//ここでタグ内のデータがChannelBufferとして取得される→native化はここを代替してやればいい
//ここでinはヘッダを読んだ後、4バイト(拡張タイムスタンプ+ストリームID)進んだ状態になって返ってくる
//ここのreadでサイズ分new byte[]アロケートされている!!!!これ重いんでないか?でもしょうがないのか?
byte[] dataAlloc = new byte[size-11];//ヘッダー+Previousを引いたデータのみのサイズ
System.arraycopy(av_frame, 11, dataAlloc, 0, size-11);//av_frameを11からコピーする時、サイズ-11以降はav_frame側の要素がない
data = ChannelBuffers.wrappedBuffer(ChannelBuffers.BIG_ENDIAN,dataAlloc);//タグのデータを突っ込む wrapped,copied,dynamicとあるが、wrappedでいいのかわからん
// byte[] ba = data.copy().toByteBuffer().array();
// Log.d("CamPreviewAtom","CamAtomDATA"+Utils.toHex(ba,0,ba.length,true));
}
public CamAtom(final byte[] header_,byte[] data_, int size) {
Log.d("CamPreviewAtom","CamAtom Called");
Log.d("CamPreviewAtom","CamAtom size " + size);
Log.d("CamPreviewAtom","CamAtom HEADER ");
// byte[] headerBytes = new byte[11];
// Log.d("CamPreviewAtom","Header "+Utils.toHex(headerBytes, 0, headerBytes.length, true));
header = readHeader(header_);
String str = "";
// for(int i = 0; i < header_.length; i++){
// if(i > 0 && i % 20 == 0)str += "\n";
// str += String.format("%02X ", header_[i]);
// }
// Log.d("NLiveRoid" ,"HEAD::::::::::::::\n"+str);
data = ChannelBuffers.wrappedBuffer(ChannelBuffers.BIG_ENDIAN,data_);//タグのデータを突っ込む wrapped,copied,dynamicとあるが、wrappedでいいのかわからん
// byte[] ba = data.toByteBuffer().array();
str = "";
// for(int i = 0; i < 10; i++){
// str += String.format("%02X ", data_[i]);
// }
// Log.d("NLiveRoid" ,"DATA::::::::::::::\n"+str);
// Log.d("CamPreviewAtom","CamAtomDATA"+Utils.toHex(ba,0,ba.length,true));
}
//カメラプレビューで呼ばれる
public RtmpHeader readHeader(final byte[] in) {
// byte[] ba = in;
byte type= in[0];
// Log.d("CamPreviewAtom","readHeader"+Utils.toHex(ba,0,ba.length,true));
final MessageType messageType = MessageType.VIDEO;
// Log.d("CamPreviewAtom","TYPE:"+messageType.toString()+" VAL:"+new String(Utils.toHexChars(type)));
//次の3バイト
final int size = (in[1] & 0xff) << 16 |
(in[2] & 0xff) << 8 |
(in[3] & 0xff) << 0;
//次の3バイト
final int timestamp = (in[4] & 0xff) << 16 |
(in[5] & 0xff) << 8 |
(in[6] & 0xff) << 0;
// 次の拡張タイムスタンプと、ストリームIDは非対応を意味する
return new RtmpHeader(messageType, timestamp, size);
}
/**
* F4vReader#next
* FlvReader#next
* FlvWriter#write
* から呼ばれる
* @return
*/
public ChannelBuffer write() {
final ChannelBuffer out = ChannelBuffers.buffer(ChannelBuffers.BIG_ENDIAN,15 + header.getSize());
out.writeByte((byte) header.getMessageType().intValue());
out.writeMedium(header.getSize());
out.writeMedium(header.getTime());
out.writeInt(0); // 4 bytes of zeros (reserved)#next
out.writeBytes(data,data.readableBytes());
out.writeInt(header.getSize() + 11); // previous tag size
return out;
}
//FileChannelがChannelBufferとして確保されたもの
//ヘッダーとして読み出すべき11バイトが入ってくる
public RtmpHeader readHeader(final ChannelBuffer in) {
byte[] ba = in.copy().array();
byte type= in.readByte();
final MessageType messageType = MessageType.VIDEO;
//次の3バイト
final int size = in.readMedium();
//次の3バイト
final int timestamp = in.readMedium();
in.skipBytes(4); // 拡張タイムスタンプと、ストリームIDは非対応を意味する
return new RtmpHeader(messageType, timestamp, size);
}
@Override
public RtmpHeader getHeader() {
return header;
}
@Override
public ChannelBuffer encode() {
return data;
}
@Override
public void decode(final ChannelBuffer in) {
data = in;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append(header);
sb.append(" data: ").append(data);
return sb.toString();
}
@Override
public MessageType getMessageType() {//AbstractMessage無くす為に新たに追加
return MessageType.VIDEO;
}
}
class PreviewMetaData implements RtmpMessage {
private final RtmpHeader header;
private ChannelBuffer data;
public PreviewMetaData() {
Log.d("NLiveRoid","MetaDataAtom From PreviewReader");
MetadataAmf0 meta = MetadataAmf0.createMetaData(liveSetting);
//タグタイプ1バイト、サイズ3バイト、タイムスタンプ3バイト、タイムスタンプ拡張1バイト、ストリームID3バイト
header = meta.getHeader();//ここでヘッダーは正しく返るはず
//ここでタグ内のデータがChannelBufferとして取得される→native化はここを代替してやればいい
//ここでinはヘッダを読んだ後、4バイト(拡張タイムスタンプ+ストリームID)進んだ状態になって返ってくる
//ここのreadでサイズ分new byte[]アロケートされている!!!!これ重いんでないか?
data = meta.encode();//メタデータのデータを突っ込む
byte[] ba = data.copy().toByteBuffer().array();
Log.d("CamPreviewAtom METADATA)",""+Utils.toHex(ba,0,ba.length,true));
//ファイル呼んでないので、スキップとか無し
}
@Override
public RtmpHeader getHeader() {
return header;
}
@Override
public ChannelBuffer encode() {
return data;
}
@Override
public void decode(final ChannelBuffer in) {
data = in;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append(header);
sb.append(" data: ").append(data);
return sb.toString();
}
@Override
public MessageType getMessageType() {//AbstractMessage無くす為に新たに追加
return null;
}
}
public int startQueueTask(){
Log.d("CamPreviewReader","startQueueTask");
//
// if(liveSetting.isUseCam() && liveSetting.isUseMic()){
// //同期をONにする
// setSync(true);
// }else{
// setSync(false);
// }
Log.d("CamPreviewReader","startedQueueTask");
return 0;
}
@Override
public MetadataAmf0 getMetadata() {
return metadata;
}
@Override
public RtmpMessage[] getStartMessages() {
return new RtmpMessage[] { metadata };
}
@Override
public boolean hasNext() {
//とりあえずtrueを返しておく
return true;
}
protected boolean hasPrev() {
//とりあえずfalseを返しておく
return false;
}
protected RtmpMessage prev() {//dataQueの1個前は取っておく?
return null;
}
//QueueにあるデータがひたすらRTMPに送信される
@Override
public RtmpMessage next() {
Log.d("CamPreviewReader","next size: "+globalQueue.size());
RtmpMessage tmp = null;
long time = System.currentTimeMillis();
while(true){
try {
tmp = globalQueue.take();
Log.d("CamPreviewReader","next#take"+tmp);
if(tmp != null){
Log.d("PreviewReader","t" + (System.currentTimeMillis() -time));
return tmp;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void close() {
liveSetting.setStreamStarted(false);
Log.d("PreviewReader","CLOSE");
new AsyncTask<Void,Void,Void>(){
@Override
protected Void doInBackground(Void... params) {
if(mic!=null&&mic.isRecording())mic.stopRecording(true);//マイクループを止める
if(liveSetting.isUseMic()&&liveSetting.isUseCam()&&mCam != null && startedPreview)mCam.stopPreview();
return null;
}
}.execute();
if(mCam!=null&&startedPreview)mCam.stopPreview();
}
@Override
public LinkedTransferQueue<RtmpMessage> getGrobalQueue() {
return globalQueue;
}
public void stopMic() {
if(mic != null)mic.stopRecording(true);
}
}