HTTP-FLV实现局域网点对点直播

使用HTTP-FLV把iPhone摄像头的画面进行直播,局域网内的设备可以通过VLC进行观看,不通过服务器,实现局域网点对点直播。
实现步骤
1、采集iPhone摄像头画面
2、采集到的数据硬编码成H264数据
3、把编码的数据通过FFmpeg封装成FLV tag
4、搭建HTTP服务器监听HTTP连接,连接成功之后发送数据

代码实现

1、采集iPhone摄像头画面

    _captureSession = [[AVCaptureSession alloc] init];

    _captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
    _captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    NSError * error = nil;
    _captureDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:_captureDevice error:&error];
    if (_captureDeviceInput) {
        [_captureSession addInput:_captureDeviceInput];
    }
    _captureVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    [_captureVideoDataOutput setAlwaysDiscardsLateVideoFrames:YES];

    NSDictionary * settings = [[NSDictionary alloc] initWithObjectsAndKeys:@(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange), kCVPixelBufferPixelFormatTypeKey, nil];
    _captureVideoDataOutput.videoSettings = settings;

    dispatch_queue_t queue = dispatch_queue_create("CaptureQueue", NULL);

    [_captureVideoDataOutput setSampleBufferDelegate:self queue:queue];
    [_captureSession addOutput:_captureVideoDataOutput];

    _previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];
    _previewLayer.frame = CGRectMake(0, 100, self.view.bounds.size.width, self.view.bounds.size.height - 100);
    [self.view.layer addSublayer:_previewLayer];
    [_captureSession startRunning];

2、采集到的数据硬编码成H264数据

    //初始化硬编码器
    OSStatus status = VTCompressionSessionCreate(NULL, 1280, 720, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)self, &_compressionSession);
    if (status != noErr) {
        NSLog(@"Create compressionSession error");
        return;
    }

    VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_High_AutoLevel);
    VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanTrue);
    VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)(@(30)));
    VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)(@(30)));
    VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)(@(800 * 1024)));
    status = VTCompressionSessionPrepareToEncodeFrames(_compressionSession);
    status = VTCompressionSessionCompleteFrames(_compressionSession, kCMTimeInvalid);
    if (status != noErr) {
        NSLog(@"Prepare error");
    }
//编码采集到的数据
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
    CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
    CMTime dur = CMSampleBufferGetDuration(sampleBuffer);
    VTEncodeInfoFlags flags;
    OSStatus status = VTCompressionSessionEncodeFrame(_compressionSession, imageBuffer, pts, dur, NULL, NULL, &flags);
    if (status != noErr) {
        NSLog(@"Encode fail");
    } 
    //此处编码也可以使用FFmpeg进行软编码CVImageBufferRef是采集出的像素数据,和CVPixelBufferRef一样,可以取出yuv数据传入FFmpeg中进行编码
}

3、把编码的数据通过FFmpeg封装成FLV tag

int ret = avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", NULL);
if (ret < 0) {
  NSLog(@"Could not allocate output format context!");
}
//这里我们通过write_buffer方法把数据写入内存通过HTTP发送出去,而不是写入文件或服务器地址,需要创建AVIOContext然后赋值给AVFormatContext
//这里申请的AVIOContext要通过avio_context_free()释放
unsigned char * outBuffer = (unsigned char *)av_malloc(32768);
AVIOContext * avio_out = avio_alloc_context(outBuffer, 32768, 1, NULL, NULL, write_buffer, NULL);
ofmt_ctx->pb = avio_out;
ofmt_ctx->flags = AVFMT_FLAG_CUSTOM_IO;

AVCodec * codec = avcodec_find_encoder(AV_CODEC_ID_H264);
out_stream = avformat_new_stream(ofmt_ctx, codec);
codec_ctx = avcodec_alloc_context3(codec);

AVRational dst_fps = {30, 1};
codec_ctx->codec_tag = 0;
codec_ctx->codec_id = AV_CODEC_ID_H264;
codec_ctx->codec_type = AVMEDIA_TYPE_VIDEO;
codec_ctx->width = 1280;
codec_ctx->height = 720;
codec_ctx->gop_size = 12;
codec_ctx->pix_fmt = AV_PIX_FMT_NV12;
codec_ctx->framerate = dst_fps;
codec_ctx->time_base = av_inv_q(dst_fps);
if(ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
  codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
ret = avcodec_parameters_from_context(out_stream->codecpar, codec_ctx);
if (ret < 0) {
  NSLog(@"Could not initialize stream codec parameters!");
}
AVDictionary * codec_options = NULL;
av_dict_set(&codec_options, "profile", "high", 0);
av_dict_set(&codec_options, "preset", "superfast", 0);
av_dict_set(&codec_options, "tune", "zerolatency", 0);

ret = avcodec_open2(codec_ctx, codec, &codec_options);
if (ret < 0) {
  NSLog(@"Could not open video encoder!");
}

out_stream->codecpar->extradata = codec_ctx->extradata;
out_stream->codecpar->extradata_size = codec_ctx->extradata_size;

ret = avformat_write_header(ofmt_ctx, NULL);
if (ret < 0) {
  NSLog(@"Could not write header!");
}
static int write_buffer(void * opaque, uint8_t * buf, int buf_size) {
  //在avformat_write_header的时候这里得到的数据是FLV文件的头部,在av_write_frame的时候这里得到的是FLV tag数据,可以通过HTTP发送出去,我使用的GCDWebServer库
  //这里buf_size上限是我们创建时的32768,如果每个tag数据小于32768就会得到完整的tag数据,如果tag数据大于32768就会得到部分tag数据,要自行处理
  return 0;
}
static void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer ) {
  //iOS硬编码的H264数据是AVCC格式的,每个NALU的前4个字节是数据长度,AVCC格式的数据直接写入文件是不能播放的,需要转换成0x00000001开始码开头的Annex B格式
  if (status != noErr) {
    NSLog(@"Compress H264 failed");
    return;
  }
  if (!CMSampleBufferDataIsReady(sampleBuffer)) {
    return;
  }
  CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
  size_t length, totalLength;
  char * dataPointer;
  const char bytesHeader[] = "\x00\x00\x00\x01";
  OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
  bool keyFrame = !CFDictionaryContainsKey((CFDictionaryRef)(CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), (const void)kCMSampleAttachmentKey_NotSync);
  NSMutableData * pktData = [NSMutableData data];
  if (keyFrame) {
    CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sample);
    size_t sparameterSetSize, sparameterSetCount;
    const uint8_t * sparameterSet;
    status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
    if (status == noErr) {
      size_t pparameterSetSize, pparameterSetCount;
      const uint8_t * pparameterSet;
      status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
      if (status == noErr) {
        size_t headerLength = 4;
        size length = 2 * headerLength + sparameterSetSize + pparameterSetSize;
        unsigned char * buffer = (unsigned char *)malloc(sizeof(unsigned char) * length);
        memcpy(buffer, bytesHeader, headerLength);
        memcpy(buffer + headerLength, sparameterSet, sparameterSetSize);
        memcpy(buffer + headerLength + sparameterSetSize, bytesHeader, headerLength);
        memcpy(buffer + headerLength + sparameterSetSize + headerLength, pparameterSet, pparameterSetSize);
        [pktData appendBytes:buffer length:length];
      }
    }
  }
  size_t bufferOffset = 0;
  int AVCCHeaderLength = 4;
  while(bufferOffset < totalLength - AVCCHeaderLength) {
    uint32_t NALUintLength = 0;
    memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
    NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
    unsigned char * buffer = (unsigned char *)malloc(sizeof(unsigned char) * (NALUnitLength + AVCCHeaderLength));
    memcpy(buffer, bytesHeader, AVCCHeaderLength);
    memcpy(buffer + AVCCHeaderLength, dataPointer + bufferOffset + AVCCHeaderLength, NALUnitLength);
    [pktData appendBytes:buffer length:NALUnitLength + AVCCHeaderLength];
    bufferOffset += AVCCHeaderLength + NALUnitLength;
  }
  AVPacket pkt = {0};
  av_init_packet(&pkt);
  pkt.data = (uint8_t *)[pktData bytes];
  pkt.size = (int)[pktData length];
  //pkt_pts从0开始递增
  pkt.pts = pkt_pts;
  pkt.dts = pkt.pts;
  if (keyFrame) {
    pkt.flags = AV_PKT_FLAG_KEY;
  } else {
    pkt.flags = 0;
  }
  pkt.stream_index = 0;
  av_packet_rescale_ts(&pkt, codec_ctx->time_base, out_stream->time_base);
  av_write_frame(ofmt_ctx, &pkt);
  pkt_pts++;
}

通过VLC可以观看直播,通过ffplay播放黑屏,原因还未发现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,454评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,553评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,921评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,648评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,770评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,950评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,090评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,817评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,275评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,592评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,724评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,409评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,052评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,815评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,043评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,503评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,627评论 2 350

推荐阅读更多精彩内容

  • ### YUV颜色空间 视频是由一帧一帧的数据连接而成,而一帧视频数据其实就是一张图片。 yuv是一种图片储存格式...
    天使君阅读 3,270评论 0 4
  • 0 概述 FFmpeg是一套领先的音视频多媒体处理开源框架,采用LGPL或GPL许可证。它提供了对音视频的采集、编...
    但行耕者阅读 6,799评论 0 19
  • 前言 如此强大的FFmpeg,能够实现视频采集、视频格式转化、视频截图、视频添加水印、视频切片、视频录制、视频推流...
    骚之哈塞給阅读 25,730评论 6 39
  • 前言 如此强大的FFmpeg,能够实现视频采集、视频格式转化、视频截图、视频添加水印、视频切片、视频录制、视频推流...
    sillen阅读 5,377评论 2 45
  • 听老师说,今天洋洋在学校里运动量比较大,中午早早就睡着了。 晚上,我七点多回到家,他见到我的第一个要求...
    Sam爸很忙阅读 291评论 0 1