投屏记录

git clone –recursive https://github.com/huang8604/huang8604.github.io.git

录屏传输

录屏方法

1726128089259.png

录屏原理

1 创建虚拟屏幕,把内容绘制到屏幕绑定的 surface 中

针对第三方的应用,由于缺少权限。

需要通过 MediaProjection 的方式创建

1
2
3
4
5
6
7
8
projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
meidaProjection = projectionManager.getMediaProjection(resultCode,data);

//创建前台通知

//mediac 创建的surface
//创建虚拟屏幕
virtualDisplay = mediaProjection.createVirtualDisplay("name",width,height,dpi,flag..mirro,surface,null,null);

对于系统应用,可以直接创建虚拟屏幕

1
virtualDisplay = ((DisplayManager)getsystemservice(DISPLAY_SERVIcE)).createvirtualDisplay("Mainscreen", width, height, dpi,surface,flgs: 1 < 10 | DisPlayManager.VIRTUAL_DISPLAY_FLAG PRESENTATION | DisPlayManage r.VIRTUAL_DISPLAY_FLKG_PUBLIC | DisPlayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
2 surface 由 mediacodec 创建
3 把 surface 内容输入到 MediaCodec,然后编码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
MediaFormat format = createFormate(... bit,fps,);
MediaCodec codec = createMediaCodec();

SurfactControl.createDisplay();
configure(codec,format);//codec.configure(format)
Surface surface = codec.createInputSurface();
setDisplaySurface(display,surface,...)
codec.start();


try{
    //开启循环
    alive = encode(codec,fd);
    code.stop();
} finally{
    destoryDisplay(diaplay);
    codec.release();
    surface.release();
}


private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
    boolean eof = false;
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

    while (!consumeRotationChange() && !eof) {
        int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
        eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
        try {
            if (consumeRotationChange()) {
                // must restart encoding with new size
                break;
            }
            if (outputBufferId >= 0) {
                ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);

                if (sendFrameMeta) {//写入文件头
                    writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
                }
				//写入帧数据
                IO.writeFully(fd, codecBuffer);
            }
        } finally {
            if (outputBufferId >= 0) {
                codec.releaseOutputBuffer(outputBufferId, false);
            }
        }
    }

    return !eof;
}

private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
    SurfaceControl.openTransaction();
    try {
        SurfaceControl.setDisplaySurface(display, surface);
        SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
        SurfaceControl.setDisplayLayerStack(display, layerStack);
    } finally {
        SurfaceControl.closeTransaction();
    }
}

private static MediaCodec  (Codec codec, String encoderName) throws IOException, ConfigurationException {
    if (encoderName != null) {
        Ln.d("Creating encoder by name: '" + encoderName + "'");
        try {
            return MediaCodec.createByCodecName(encoderName);
        } catch (IllegalArgumentException e) {
            Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
            throw new ConfigurationException("Unknown encoder: " + encoderName);
        } catch (IOException e) {
            Ln.e("Could not create video encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage());
            throw e;
        }
    }

    try {
        MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
        Ln.d("Using video encoder: '" + mediaCodec.getName() + "'");
        return mediaCodec;
    } catch (IOException | IllegalArgumentException e) {
        Ln.e("Could not create default video encoder for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage());
        throw e;
    }
}
录屏保存为文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
String rootDir = Applistion.getInstance().getExternalCacheDir()+"...";
File file = new File(path+"test.h264");
FileOutputStream fileOutputStream = null;
fileOutputStream = new FileOutputStream(file);

fileOutputStream.write(data);

fileOutstream.flush();
fileOutStream.close();
fileOutStream = null;
file = null;
录屏数据编码传输

服务端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ServerSocket mServer = null;
Socket mSocket = null ;

void init() {
    new Thread (new Runnable(){
        @Override
        public void run(){
            while(true){
                try{
                    if(mServer == null){
                        //新建服务socket 服务
                        mServer = new ServerSocker(6666);
                        mSocket = mServer.accept();

                        //建立连接后写入数据
                        mSocket.getOutputStream().write(66);
                    }
                }catch(Exception e){
                    e.pri...
                }

            }
        }
    }).start();

    public Socket getSocket(){
        return mSocket;
    }
}


ConnectionManager.getInstance().getSocket().getOutPutStream().write(data);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
MediaCodeec.BufferInfo buffInfo = new MediaCode.BufferInfo();
while(running && !eof){
    int outputBufferId = codec.dequeueOutputBuffer(bufferInfo,timeouts-1);
    eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
    try{
        if(outBufferId >=0){
            ByteBuffer codeBuffer = codec.getoutputBuffer(outputBufferId);
            byte[] data = new byte[codecBuffer.remaining()];
            codecBuffer.get(data);


            int h264Len = data.length;
            ByteBuffer dataBuffer = ByteBuffer.allocate(4);
            dataBuffer.putInt(h264Len);
            dataBuffer.flip();
            byte[] dataLen = new byte(4);
            dataLenBuf.get(dataLen);

            ConnectionManager.getInstance().getSocket().getOutputStream().write(dataLen);
            ConnectionManager.getInstance().getSocket().getOutputStream().write(data);
        }

    }finally{
        if(outBufferId >=0){
            codec.releaseOutputBuffer(outBufferId,false);
        }
    }
}

客户端的接受

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
Socket mClientSocket = null;

new Thread(new Runnable(){
     @Overrride
     public void run(){
     	try{
            mClientSocket = new Socket(ip,port 6666);
            byte[] data = new byte[1];
            int len = mClientSocket.getInputStream.read(data);
            //接受一个头字节
            if(len == 1 && data[0] == 66){
                //ok
                runOnUiThread(new Runnable(){
                    @Override
                    public void run(){
                        //...
                    }
                });
            }

            while(running){
                byte[] headLen = new byte[4];
                int readLen = 0;
                while(readLen < headLen.length){
                    readLen = readLen + mClientSocket.getInputStream.read(headLen,readLen,headLen.length-readLen);
                }
                //读取的数组 转为 长度
				int h264Len = byteArrayToInt(headLen);
                byte[] rawData = new byte[h264Len];

                int rawDataLen = 0;
                while(rawDataLen < h264Len){
                    rawDataLen = rawDataLen + mClientSocket.getInputStream.read(rawData,rawDataLen,h264Len-rawDataLen);
                }

                //rawData 就是每一帧的图像数据
                //生产放入数组
                synchronized (mH264List){
                    mH264List.add(rawData);
                    //消费者可能在等待
                    mH264List.notify();
                }

                //保存文件
                // fileOutputStream.write(h264Data,0,readLen1);
             }

        }catch(Exception e){
            e.pritStack
        }

     }


 }).start();


public static int byteArrayToInt(byte[] bytes) {
    // 确保输入的byte数组长度为4
    if (bytes.length != 4) {
        throw new IllegalArgumentException("Byte array must be 4 bytes long");
    }

    return (bytes[0] & 0xFF) << 24 |
           (bytes[1] & 0xFF) << 16 |
           (bytes[2] & 0xFF) << 8  |
           (bytes[3] & 0xFF);
}

public static int byteArrayToInt(byte[] bytes) {
    // 确保输入的byte数组长度为4
    if (bytes.length != 4) {
        throw new IllegalArgumentException("Byte array must be 4 bytes long");
    }

    // 使用 ByteBuffer 将 byte[] 转换为 int
    ByteBuffer buffer = ByteBuffer.wrap(bytes);
    return buffer.getInt();
}

public static int byteToInt2(byte[] src,int offset){

    return value;
}
录屏数据解码显示

1726134395165.jpg

  1. client 先从 codec 请求 空的 input buffer,填满

  2. 送给 codec,codec 内部再解码

  3. 显示端从 codec 取出 outputbuffer ,opengl 绘制,或者调用接口处理

1726134667073.jpg

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
MediaCodec mediaCodec = null ;
void inintMediaCode(){
    try{
        mediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYE_VIDEO_AVC);
    	MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC);
        mediaCodec.configure(format,surface,null,0);
        mediaCodec.start();

    }catch (Exception e){
        e.print...
    }
}

ArrayList<byte[]> mH264List = new ArrayList();

void decodeH264(){
    new Thread(new Runnable(){
        @Override
        public void run(){
          if(mediacode != null){
              while(running){
               	//消费
                byte[] rawH264 = null;
                synchronized (mH264List){
                    if(mH264List,size() > 0){
                        rawH264 = mH264List.get(0);
                        mH264List.remove(0);
                    }
                    if(rawH264 == null){
                        try{
                            //wait的时候,锁回释放掉
                            mH264List.wait();
                        }catch(){

                        }
                        continue;
                    }
                }

                //解码
                ByteBuffer[] intputBuffer = mediaCode.getInputBuffers();
                int inputIndex = -1;
                while(inputIndex < 0){
                    inputIndex = mediaCode.deququeInputBuffer(timeouts 10000);
                }
                ByteBuffer bytebuffer = inputBuffers[inputIndex];
                bytebuffer.clear();
                bytebuffer.put(rawH264);

                mediaCode.queueInputBuffer(inputIndex,0,rawH264.lenth,0,0);
                //渲染到surface
                MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
                int outputIndex = mediaCode.dequeueOutputBuffer(info,timeout 10000);
                if(outputIndex >= 0){
                    mediaCode.releaseOutputBuffer(outputIndex,true);//render = true ,就会渲染
                }
              }

          }
        }
    }).start();


}

//创建一个SurfaceView
  SurfaceView surfaceView = findViewById(...);
  SurfaceView.getHolder().addCallback(new SurfaceHolder.callback(){

      public void surfaceChanged( holder,format,width,height)
      initMeidaCodec(holder.getSurface(),w,h);
  });

投屏相关

创建虚拟屏幕有一些FLAG

同显模式 mirror
异显模式 OWN_CONTENT
1
virtualDisplay = ((DisplayManager)getsystemservice(DISPLAY_SERVIcE)).createvirtualDisplay("Mainscreen", width, height, dpi,surfaceHags: 1 < 10 | DisPlayManager.VIRTUAL_DISPLAY_FLAG PRESENTATION | DisPlayManager.VIRTUAL_DISPLAY_FLKG_PUBLIC | DisPlayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);

surfaceHags: 1 < 10 其实为VIRTUAL_DISPLAY_FLAG_TRUSTED flag

VIRTUAL_DISPLAY_FLAG_SECURE ,没有该flag,截图或者支付等界面是不允许打开在该屏幕

不同尺寸的投屏
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//服务端
public class Config{
    public static int sWidth = 1280;
    public static int sHeight = 720;
    public static int sDpi = 213;
}
createvirtualDisplay 有相应的参数 
    
//客户端
获取屏幕宽高
DisplayMetries metries = getResource().getDisplayMetrics();
int widthDiplay = metrics.widthPixels;
int heightDiplay = metrics.heightPixels;

ViewGroup.LayoutParams layoutParams = surfaceView.getLayoutParams();
layoutParams.width = w;
layoutParams.height = h;

surfaceView.setLayoutParams(layoutParams);
    
runOnUiThread(new Runnable(){
    @Overrride
    public void run(){
        resize();
    }
});

异屏幕启动方式

一 Dialog
三 Activity
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void startActivityOtherDisplay(Context context){
	DisplayManager mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
	Display[] displays = mDisplayManger.getDisplays();

	if(display.length > 1){
	  Intent intent = new Intent();
		intent.setClass(context,MainActivity.class);

	  AcitivityOptions options = ActivityOptions.makeBasic();
	  //设置最后一个屏幕的id,这里可以根据name等特征值设置ID
	  options.setLaunchDisplayId(displays[displays.length -1].getDisplayId());

	  context.startActivity(intent,options.toBunndle());
	}else{
		Log.d("test","only one display");
	}


}

触摸部分

触摸事件注入
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

MotionEvent event = MotionEvent.obtain(now,now,action,pointCount,coords,metaState 0,buttonState 0,xPrecision 1 ,yPresision 1,deviceID:0,edgeFlags 0,InputDevice.SOURCE_TOUCHSCREEN,flags 0)

//hide 方法,apk需要通过反射调用
MotionEvent.setDisplayId(1);

//反射方法 begin
public static void setDisplayId(MotionEvent event,int displayId){
   
    try{
      Mothed method = MotionEvent.class.getMethod("setDisplayId",int.class);
	  method.invoke(event,displayId);
    }catch(){}
}
//反射方法 end

    
Instrumentation instrumentation = new Instrumentation();
instrumentaion.sendPointerSync(MotionEvent)

音频部分

服务端
AudiRecord 版本
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int AUDIO_SOURCE = MediaRecorder.AudioSOURCE.REMOTE_SUBMIX;
publicint CHANNEL = AUdiOFORMAT.CHANNEL_IN_STEREO;
static finalint AUDIO_FORMAT = AUdioFOrmat.ENCODING_PCM_16BIT;
public static finalint SAMPLE RATE = 44100;
private static AudioDataManager sAudioDataManager= new AudioDataManager();
boolean mRecording = true;
AudioRecord audioRecord = null;
int buffersize = 0;


void startRecord(final Socket socket){
    mRecording = true;
    buffersize = AudioRecord.getMinBUfferSiZe(SAMPLE RATE, CHANNEL ,AUDIO FORMAT);
    audioRecord = new AudioRecord(AUDIO SOURCE,SAMPLE RATE, CHANNEL,AUDIO_FORMAT,buffersize);
    audioRecord.startRecording();
    new Thread(new Runnable(){
        @0verride
        public void run(){
            while(mRecording){
                bytel] buffer = new byte[buffersize];
                int len = audioRecord.read(buffer, offsetinBytes: o,buffer.length);
                if (len > 0){
                try {
                	if(socket != null){
                        socket.getoutputstream().write(buffer, off: 0,len);
                }catch (Exception e){
                	e.printstackTrace();
                }
}).start();
优化版本:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
String ADDRESS = "audio.send";
LocalserverSocket mLocalServerSocket = null;
Localsocket msocket = null;



void startRecordsocket(Socket socket){
	testplay();
	mRecording = true;
    bufferSize = AudioRecord.getMinBUfferSiZe(SAMPLE RATE,CHANNEL,AUDIO FORMAT);
    audioRecord = NeW AUdIOReCOrd(AUDIO SOURCE,SAMPLE RATE, CHANNEL,AUDIO FORMAT,buffersize);audioRecord.startRecording();
    new Thread(((Runnable)(){
        
        
            try {
                if(mLocalserverSocket != null){
                    mLocalserverSocket.close();
                    mLocalserverSocket = null;
                }
                if(mSocket != null) mSocket.close();
				mLocalserverSocket = new LocalServerSocket(ADDRESS);
                mSocket = mLocalserverSocket.accept();
                //组塞式读取数据
                while(mRecording){
                    byte[] data = new byte[1024 * 4];
					int len = 0;
					while (len < data.length){
                        //获取数据
                        len = len + mSocket.getInputstream().read(data,len, len: data.length len);
                        if(len > 0){
                            if(socket != null){
                                //将获取的数据发送到客户端
                                socket.getOutputStream().write(data,off:0,len )
                            }
                        }
                    }          
            }catch (Exception e){
				e.printstackTrace();
     ).start();
}

av/services/audioflinger/THreads.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
int connect_fd = -1;
#define  ADDRESS  "audio.send"
    
sizz_t AudioFLinger::PlaybackThread::threadLoop_write()
    
sszie_t frameWritten = mNormalSink-write ...

//add begin 
    //include <sys/socket.h>
    //include <sys/un.h>
//需要区分 REMOTE_SUBMIX     
// isSingleDeviceTyep(outDevices,AUDIO_DEVICE_OUT_REMOTE_SUBMIX)
if(connect_fd <= 0){
    connect_fd = SOCket(AF UNIX,SOCK STREAM,0);
    if(connect fd < 0){
         ALoGI("init socket fail");
         return 1;
	 }
    sockaddr_un srv_addr{};
    memset(&srv addr,0,sizeof(srv addr));
    srv_ddr.sun_path[0]='\0'; //注意
	strcpy(srv_addr.sun_path + 1, ADDRESS);
 
	srv_addr.sun_family = AF_UNIX;
 
    int nameLen = strlen(ADDRESS);
    int len =1+ nameLen + offsetof(struct sockaddr_un, sun_path);

    int ret = connect(connect_fd,(struct sockaddr*)&srv addr, len);
     if(ret == -1){
         ALOGI("connect socket fail:%d",ret);
         connect fd=-1;
     } else {
        AL0GI("connect success,start thread");
     }
}else{
    if(framesWritten > 0){
        int result = ::send(connect_fd,(char *)mSinkBuffer + offset,framesWriiten*mFrameSize,0);
        if(result < 0){
            ::close(connect_fd);
            connect_fd = -1;
            ALOGI("debug this ");
        }
    }
}
//add end
    
    
if(frameWritten > 0)
    ...
    

1726221633561.png

客户端

1726222556642.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void receiveData(final socket socket){
    if (socket != null){
        final byte[] buf = new byte[buffersize];
        new Thread(new Runnable(){
            @override
       		public void run(){
                while(mRunning){
                try {
                    int len = socket.getInputstream().read(buf);
                    playAudio(buf,len);
                }catch (Exception e){
                    e.printstackTrace();
                }
}).start();

                
void playAudio(byte[] data,int len){
    if(mAudioTrack != null){
        mAudioTrack.play();
        mAudioTrack.writ(data,offset:0,len)
    }
}

ByteBuffer 介绍

1726133220888.jpg
1726133235968.jpg
1726133244889.jpg
1726133272357.jpg
1726133287900.jpg

功能再优化

视频延时测试验证

1726211690491.jpg

悬浮窗口
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 SytemTimeDialog
SystemTimeDialog(Contextcontext){
	mContext = context;// 取得系统窗体
	mWindowManager = (WindowManager)context.getSystemservice(ConteXt.WINDOW SERVICE);
    // 窗体的布局样式
    mLayoutParams = new WindowManager.LayoutParams();
    mLayoutParams.setTitle("TimeShow");
    //设置窗体显示类型-TYPE APPLICATION OVERLAY
    mLayoutparams.type = Windowanager.LayoutParamS.TYPE_APPLICATION_OVERLAY;//2018; //windowManager.LayoutParams.TYPE POINTER;mLayoutParams.flags = windowanager.LayoutParamS.FLAG NOT TOUCH MODAL| WindowManager.LayoutParaMS.FLAG NOT FOCUSABLE;mLayoutParams.width = WindowManager.LayoutParamS.WRAP CONTENT;
    mLayoutParams.height = WindowManager.LayoutParamS.WRAP_CONTENT;
    mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
    //mLayoutParams.privateFlags = mLayoutParams.priVateFLagS | PRIVATE FLAG SYSTEM APPLICATION OVERLAY;
    timeTextView = new TextView(mContext);
    timeTextView.setTextColor(Color.RED);
    timeTextView.setTextsize(20);
    timeTextView.setText("HelloWorld")
    timeTextView.setOnTouchListener(onTouchDrawListener)

 //更新拖拽
 mWindowManager.updateViewLayout(v.mLayoutParams);   
    
     
 //更新显示毫秒的悬浮窗    
 void show(){
	ValueAnimator animator = ValueAnimator.ofInt(0,100);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
        @0verride
        public void onAnimationUpdate(ValueAnimator animation){
            long time = System.currentTimeMillis();
        	SimpleDateFormat simpleDateFormat = new SimpleDateFormat( pattern: "HH:mm:ss :SSS" );
            String showTime = simpleDateFormat.format(time);
            timeTextView.setText(showTime);
        	timeTextView.setBackgroundcolor(Color.BLACK)|
        });
        animator.setDuration(2000);
        animator.setRepeatCount(-1);
        animator.start();

 	mWindowManager.addView(timeTextView,mLayoutParams);       
 }       
音频延迟测试方案

source 端和 sink端 同时发声,再录音,分析波峰的间隔时间。

内录的方案改造成播放声音。

audiopolicy/enginedefault/src/Engine.cpp

1726215782660.png

选择合适的音源播放,比如拨号。声音大小要有区分

录制投屏的声音文件。

通过 audacity软件 进行mp3波形分析

1726216100637.png

竞品的数据如何采集

adb shell dumpsys meida_audio_flinger > 1.txt

查看 standby 为 no,查看发声output devices

优化方案
视频方面
  1. 尽量多线程,数据生产,解码,渲染单独运行

    1726213175003.jpg

  2. 多线程 生产者的数据集合需要进行统计集合大小。在卡顿的时候,可以方便定位。

  3. a. 更加体验和硬件性能选择 码率和分辨率。

    b. 根据生产者消费者模型,定位挤压数据位置。 针对瓶颈考虑丢帧。比如网络卡顿,突然来了很多帧,可以考虑跳过数据

    c. 如果是软解码,如使用ffmpeg等,可以考虑多线程解码

音频方面

AudioRecord 自带100ms延时

1726217384446.png

  1. 在 AudioTrack 中拦截,直接发送出去

    优点:速度快

    缺点 : AudioTrack 有多数据源的写入。数据源参数采样率声道等都不一样

    复杂度比较高

  2. 在AudioFlinger 中进行拦截,不走 AudioRecord

1726217928099.png

1726218239318.png

1726218375306.png

1726218413560.png

1726218576997.png
1726218638822.png

还需要保持REMOTE_SUB_MIX,让OutputThread 合成一个音轨。

1726218976299.png

强制桌面模式

可以强制异显屏幕显示出桌面

-–end–

0%