Android无预览(后台)录制、推流问题点记录

本文详细介绍了如何在Android应用中实现在无预览页面的情况下,通过MediaCodec和rtmp-rtsp-stream-client-java库进行camera数据获取、编码、推流和录制,同时解决了角度调整的问题。实践中通过创建无界面的SurfaceTexture获取camera数据,并利用MediaCodec进行编码,最终实现后台推流和录像功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求


       实现一个无预览页面,在后台推流、录像、拍照等操作的APP应用。

方案分析


       客户需求看似非常简单一句话,实际却是一句非常完美的将程序员产出无限压榨的标准的资本家日常话术。在这么极简的需求面前,是个程序员都不好意思把工作量报成一个月。

       好吧!废话不多说,我们来分析一下就这个需求需要实现或者优化哪些技术点。

  • 毋庸置疑,第一个肯定是无预览获取camera数据;
  • 由于获取的是原始数据,所以我们需要进行编码(包含video和audio);
  • 由于我们要实现录制、推流、拍照等,所以需要对编码数据进行封装(如flv、mp4、jpeg等);
  • 最后进行录制、推流、拍照等操作;

实践


1、无预览获取camera数据

       一开始想无预览还不简单,直接把用于渲染的SurfaceView、GLSurfaceView、TextureView等控件设置1像素不就可以了,想要实现后台偷偷打开,弄个1像素悬浮窗就好了。
       但这么弄客户就不乐意了,原因一是得申请权限、二担心耗电、三还有1像素view心里肯定不舒服。
       最后通过去翻了官网API、问度娘、问谷歌、问群里大佬得知MediaCodec有个createInputSurface可以获取一个无界面的Surface,把这个塞给camera不就好了(CSDN上有大佬证实过可行性)。考虑到实际应用场景中,不一定都能支持硬解,但看到这里就可以照葫芦画瓢了,自己new一个SurfaceTexture一样塞给camera,通过监听onPreviewFrame获取YUV数据就可以了。

//new SurfaceTexture
SurfaceTexture surfaceTexture = new SurfaceTexture(0);
//打开摄像头
Camera mCamera = Camera.open(mCamId);
//设置camera参数
Camera.Parameters params = mCamera.getParameters();
params.setZoom(0);
params.setPreviewFormat(ImageFormat.NV21);
params.setPreviewSize(width, height);
params.setPictureSize(width, height);
、、、、、、
mCamera.setParameters(params);
//设置回调用于获取摄像头数据
mCamera.setPreviewCallback(mPreviewCallback );
try {
 //这一步是最关键的,使用surfaceTexture来承载相机的预览,而不需要设置一个可见的view
 	mCamera.setPreviewTexture(surfaceTexture);
    mCamera.startPreview();
} catch (IOException ioe) {
      ioe.printStackTrace();
}

监听回调方法获取摄像头数据,再进行编码、录制等操作

final Camera.PreviewCallback mPreviewCallback = new Camera.PreviewCallback() {
	@Override
	public void onPreviewFrame(byte[] data, Camera camera) {
		//进行编码、录像等操作
	}
 };

2、编码、推流、录制

       推流、编码常用的也就是MediaCodec、FFmpeg、librtmp,赶项目的程序员都不会重复造轮子,我这里借鉴的rtmp-rtsp-stream-client-java

3、问题点

       一开始没有用rtmp-rtsp-stream-client-java来改,自己DIY的编解码和推流,出现角度问题没法通过setDisplayOrientation来调整角度,尝试对onPreviewFrame返回的YUV调整角度后再进行推流,但出现四重影问题难解决,后面借鉴rtmp-rtsp-stream-client-java,封装好的角度调整,可以完美解决,不过直接在rtmp-rtsp-stream-client-java改更加方便。

public class YUVUtil {


  public static void preAllocateBuffers(int length) {
    NV21Utils.preAllocateBuffers(length);
    YV12Utils.preAllocateBuffers(length);
  }

  public static byte[] NV21toYUV420byColor(byte[] input, int width, int height,
      FormatVideoEncoder formatVideoEncoder) {
    switch (formatVideoEncoder) {
      case YUV420PLANAR:
        return NV21Utils.toI420(input, width, height);
      case YUV420SEMIPLANAR:
        return NV21Utils.toNV12(input, width, height);
      default:
        return null;
    }
  }

  public static byte[] rotateNV21(byte[] data, int width, int height, int rotation) {
    switch (rotation) {
      case 0:
        return data;
      case 90:
        return NV21Utils.rotate90(data, width, height);
      case 180:
        return NV21Utils.rotate180(data, width, height);
      case 270:
        return NV21Utils.rotate270(data, width, height);
      default:
        return null;
    }
  }

  public static byte[] YV12toYUV420byColor(byte[] input, int width, int height,
      FormatVideoEncoder formatVideoEncoder) {
    switch (formatVideoEncoder) {
      case YUV420PLANAR:
        return YV12Utils.toI420(input, width, height);
      case YUV420SEMIPLANAR:
        return YV12Utils.toNV12(input, width, height);
      default:
        return null;
    }
  }

  public static byte[] rotateYV12(byte[] data, int width, int height, int rotation) {
    switch (rotation) {
      case 0:
        return data;
      case 90:
        return YV12Utils.rotate90(data, width, height);
      case 180:
        return YV12Utils.rotate180(data, width, height);
      case 270:
        return YV12Utils.rotate270(data, width, height);
      default:
        return null;
    }
  }

  public static Bitmap frameToBitmap(Frame frame, int width, int height, int orientation) {
    int w = (orientation == 90 || orientation == 270) ? height : width;
    int h = (orientation == 90 || orientation == 270) ? width : height;
    int[] argb = NV21Utils.toARGB(rotateNV21(frame.getBuffer(), width, height, orientation), w, h);
    return Bitmap.createBitmap(argb, w, h, Bitmap.Config.ARGB_8888);
  }

  public static byte[] ARGBtoYUV420SemiPlanar(int[] input, int width, int height) {
    /*
     * COLOR_FormatYUV420SemiPlanar is NV12
     */
    final int frameSize = width * height;
    byte[] yuv420sp = new byte[width * height * 3 / 2];
    int yIndex = 0;
    int uvIndex = frameSize;

    int a, R, G, B, Y, U, V;
    int index = 0;
    for (int j = 0; j < height; j++) {
      for (int i = 0; i < width; i++) {

        a = (input[index] & 0xff000000) >> 24; // a is not used obviously
        R = (input[index] & 0xff0000) >> 16;
        G = (input[index] & 0xff00) >> 8;
        B = (input[index] & 0xff) >> 0;

        // well known RGB to YUV algorithm
        Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
        U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
        V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;

        // NV21 has a plane of Y and interleaved planes of VU each sampled by a factor of 2
        //    meaning for every 4 Y pixels there are 1 V and 1 U.  Note the sampling is every other
        //    pixel AND every other scanline.
        yuv420sp[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
        if (j % 2 == 0 && index % 2 == 0) {
          yuv420sp[uvIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
          yuv420sp[uvIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
        }

        index++;
      }
    }
    return yuv420sp;
  }

  public static byte[] CropYuv(int src_format, byte[] src_yuv, int src_width, int src_height,
      int dst_width, int dst_height) {
    byte[] dst_yuv;
    if (src_yuv == null) return null;
    // simple implementation: copy the corner
    if (src_width == dst_width && src_height == dst_height) {
      dst_yuv = src_yuv;
    } else {
      dst_yuv = new byte[(int) (dst_width * dst_height * 1.5)];
      switch (src_format) {
        case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: // I420
        case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar: // YV12
        {
          // copy Y
          int src_yoffset = 0;
          int dst_yoffset = 0;
          for (int i = 0; i < dst_height; i++) {
            System.arraycopy(src_yuv, src_yoffset, dst_yuv, dst_yoffset, dst_width);
            src_yoffset += src_width;
            dst_yoffset += dst_width;
          }

          // copy u
          int src_uoffset = 0;
          int dst_uoffset = 0;
          src_yoffset = src_width * src_height;
          dst_yoffset = dst_width * dst_height;
          for (int i = 0; i < dst_height / 2; i++) {
            System.arraycopy(src_yuv, src_yoffset + src_uoffset, dst_yuv, dst_yoffset + dst_uoffset,
                dst_width / 2);
            src_uoffset += src_width / 2;
            dst_uoffset += dst_width / 2;
          }

          // copy v
          int src_voffset = 0;
          int dst_voffset = 0;
          src_uoffset = src_width * src_height + src_width * src_height / 4;
          dst_uoffset = dst_width * dst_height + dst_width * dst_height / 4;
          for (int i = 0; i < dst_height / 2; i++) {
            System.arraycopy(src_yuv, src_uoffset + src_voffset, dst_yuv, dst_uoffset + dst_voffset,
                dst_width / 2);
            src_voffset += src_width / 2;
            dst_voffset += dst_width / 2;
          }
        }
        break;
        case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: // NV12
        case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar: // NV21
        case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
        case MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar: {
          // copy Y
          int src_yoffset = 0;
          int dst_yoffset = 0;
          for (int i = 0; i < dst_height; i++) {
            System.arraycopy(src_yuv, src_yoffset, dst_yuv, dst_yoffset, dst_width);
            src_yoffset += src_width;
            dst_yoffset += dst_width;
          }

          // copy u and v
          int src_uoffset = 0;
          int dst_uoffset = 0;
          src_yoffset = src_width * src_height;
          dst_yoffset = dst_width * dst_height;
          for (int i = 0; i < dst_height / 2; i++) {
            System.arraycopy(src_yuv, src_yoffset + src_uoffset, dst_yuv, dst_yoffset + dst_uoffset,
                dst_width);
            src_uoffset += src_width;
            dst_uoffset += dst_width;
          }
        }
        break;

        default: {
          dst_yuv = null;
        }
        break;
      }
    }
    return dst_yuv;
  }
}

实现效果


手机端,开始推流后直接回到桌面任可以继续推流;
在这里插入图片描述
SRS播放端,演示在1秒以内;
在这里插入图片描述

项目地址


https://github.com/pedroSG94/rtmp-rtsp-stream-client-java

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

玉念聿辉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值