• 王学岗————从零实现手写音视频通话(H265)


    webrtc

    WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC 包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能。
    A和B进行视频通话,A对摄像头数据(yuv)编码成h264传给B。B也做同样的事情。与投屏不一样的是,A与B是双向的。
    为了简单方便,我们今天构建两个项目。两个APK

    转换

    nv21转成nv12

     static  byte[] nv12;
        /**
         * yyyyyyyy uvuvuvuvuv nv21 android摄像头
         * yyyyyyyy uuuuuvvvvv nv12(又叫yuv420)
         */
        public static byte[]  nv21toNV12(byte[] nv21) {
    // 数组的长度分成三部分,y的长度是1,u的长度是1/4,v同U
            int  size = nv21.length;
            nv12 = new byte[size];
            //y占总长度的三分之二
            int len = size * 2 / 3;
            System.arraycopy(nv21, 0, nv12, 0, len);
            int i = len;
            while(i < size - 1){
                nv12[i] = nv21[i + 1];
                nv12[i + 1] = nv21[i];
                i += 2;
            }
            return nv12;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    摄像头旋转

    在这里插入图片描述

    //摄像头旋转算法,要旋转90度
        public static void portraitData2Raw(byte[] data,byte[] output,int width,int height) {
            int y_len = width * height;
            int uvHeight = height >> 1; // uv数据高为y数据高的一半
            int k = 0;
            for (int j = 0; j < width; j++) {//j是行,i是列
                for (int i = height - 1; i >= 0; i--) {
                    output[k++] = data[ width * i + j];
                }
            }
            for (int j = 0; j < width; j += 2) {
                for (int i = uvHeight - 1; i >= 0; i--) {
                    output[k++] = data[y_len + width * i + j];
                    output[k++] = data[y_len + width * i + j + 1];
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    音频

    1,音频的源数据是pcm,编码后可以是aac。acc是压缩数据。
    在这里插入图片描述

    上图中,从input.mp4中提取aac和pcm文件,可以发现,pcm文件大于aac文件

    2,音频是声波。
    sin函数,cos函数可以存储声波。
    2,声音波形一般比较复杂,需要用一系列数学表达式存储。编码解码会耗费cpu很大算力,这样存储传输不划算。
    在这里插入图片描述

    3,我们通过采样来存储声波。问题是间隔多少采样合适?人耳能听到的范围是20-20万HZ。很长一个周期就是低频。采样用的是奈奎斯特定理。最高频的两倍
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    一秒钟采样22.05kX2 =44100个点
    4,一个字节8位,两个字节16位(-32678-32767)。
    在这里插入图片描述

    5,在这里插入图片描述
    6,在这里插入图片描述
    在这里插入图片描述

    音频代码

    录制的声音16进制数据
    在这里插入图片描述

    package com.maniu.webrtcmaniua;
    
    import android.Manifest;
    import android.content.Context;
    import android.content.pm.PackageManager;
    import android.media.AudioFormat;
    import android.media.AudioManager;
    import android.media.AudioRecord;
    import android.media.AudioTrack;
    import android.media.MediaRecorder;
    import android.os.Handler;
    import android.os.HandlerThread;
    import android.util.Log;
    
    import androidx.core.app.ActivityCompat;
    
    import java.io.FileInputStream;
    
    public class AudioRecoderLive {
        private AudioTrack audioTrack;
        SocketLive socketLive;
        AudioRecord audioRecord;
        // 采样率,现在能够保证在所有设备上使用的采样率是44100Hz, 但是其他的采样率(22050, 16000, 11025)在一些设备上也可以使用。
        public static final int SAMPLE_RATE_INHZ = 44100;
        // 声道数。CHANNEL_IN_MONO and CHANNEL_IN_STEREO. 其中CHANNEL_IN_MONO是可以保证在所有设备能够使用的。
        public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
        // 返回的音频数据的格式。 ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, and ENCODING_PCM_FLOAT.
        public static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    //    int bufferSizeInBytes
        boolean isRecording = false;
        HandlerThread handlerThread;
        Handler workHandler;
        public AudioRecoderLive(SocketLive socketLive) {
            this.socketLive = socketLive;
        }
        public void startRecord(Context context) {
            handlerThread = new HandlerThread("handlerThread");
            handlerThread.start();
            workHandler = new Handler(handlerThread.getLooper());
            final int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE_INHZ, CHANNEL_CONFIG, AUDIO_FORMAT);
            //申请录音权限
            if (ActivityCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
    
                return;
            }
            //通过AudioRecord采样
            audioRecord = new AudioRecord(
                    MediaRecorder.AudioSource.MIC,//音频的来源,我们选择录制MIC麦克风
                    SAMPLE_RATE_INHZ,//采样频率
                    CHANNEL_CONFIG,//声道数,一般是双通道
                    AUDIO_FORMAT,//采样位数,2个字节16位
                    minBufferSize);//声音最小数据量
    
            audioRecord.startRecording();
            //录音是耗时操作,开启个子线程
            isRecording = true;
            // 初始化缓存
            final byte[] data = new byte[minBufferSize];
            // 创建数据流,将缓存导入数据流
            workHandler.post(new Runnable() {
                @Override
                public void run() {
                    while (isRecording) {
                        //麦克风数据放到data容器中
                        int length = audioRecord.read(data, 0, minBufferSize);
                        YuvUtils.writeContent(data);
                        YuvUtils.writeBytes(data);
                        socketLive.sendData(data, 0);
                    }
                }
            });
        }
        public void initPlay() {
            Log.i("Tag8","go there");
            //配置播放器
            //音乐类型,扬声器播放
            int streamType = AudioManager.STREAM_MUSIC;
            //录音时采用的采样频率,所有播放时同样的采样频率
            int sampleRate = 44100;
            //单声道,和录音时设置的一样
            int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
            // 录音使用16bit,所有播放时同样采用该方式
            int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
            //流模式
            int mode = AudioTrack.MODE_STREAM;
    
            //计算最小buffer大小
            int minBufferSize=AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat);
            //AudioTrack是用来播放的。
            //构造AudioTrack  不能小于AudioTrack的最低要求,也不能小于我们每次读的大小
            audioTrack=new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,
                    minBufferSize,mode);
            audioTrack.setVolume(16f);
            //从文件流读数据
            FileInputStream inputStream = null;
            audioTrack.play();
        }
    
        public void doPlay( byte[] mBuffer) {
         
            int ret = audioTrack.write(mBuffer, 1, mBuffer.length-1);
            Log.i("Tag8","ret ==="+ret);
            //检查write的返回值,处理错误
            switch(ret) {
                case AudioTrack.ERROR_INVALID_OPERATION:
                case AudioTrack.ERROR_BAD_VALUE:
                case AudioManager.ERROR_DEAD_OBJECT:
                    return;
                default:
                    break;
            }
            Log.i("Tag8","播放成功。。。。");
    
        }
    
    }
    
    
    • 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
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
  • 相关阅读:
    react-router 如何在组件外路由跳转
    电子招标采购商城系统:优化传统采购业务,提速企业数字化升级
    调用线程的run()和start()方法有什么不同呢?
    基于复合粒子群优化的模糊神经预测控制的研究(Matlab代码实现)
    np.random.seed(), torch.manual_seed(args.seed)
    项目十二认识指针
    yolov5 obb旋转框 tensorrt部署
    OpenAI官方吴达恩《ChatGPT Prompt Engineering 提示词工程师》(3)摘要
    常用gdb调试命令
    VCS工具学习笔记(3)
  • 原文地址:https://blog.csdn.net/qczg_wxg/article/details/126183913