me icon indicating copy to clipboard operation
me copied to clipboard

学习 rtmp 开发 (Part 7: push streaming from FLV)

Open nonocast opened this issue 2 years ago • 0 comments

replay.c (254 lines)

#include <assert.h>
#include <librtmp/amf.h>
#include <librtmp/log.h>
#include <librtmp/rtmp.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

typedef unsigned char byte;
enum tag_types { TAGTYPE_AUDIODATA = 8, TAGTYPE_VIDEODATA = 9, TAGTYPE_SCRIPTDATAOBJECT = 18 };
const char *flv_tag_types[] = {"", "", "", "", "", "", "", "", "audio", "video", "", "", "", "", "", "", "", "", "script data"};

typedef struct flv_tag {
  byte type;
  byte head[11];
  uint32_t offset;
  size_t size;
  uint32_t data_offset;
  size_t data_size;
} flv_tag_t;

void open_flv();
void open_rtmp();
void close_rtmp();
void send_metadata();
void send_metadata_packet();
void send_video_tag(uint32_t);
void get_metadata_tag();
void get_video_tags();
flv_tag_t *read_tag();
void read_ui24(uint32_t *, void *);
int die();
static int DumpMetaData(AMFObject *);
static void sigIntHandler(int sig);

// variable
FILE *infile;
RTMP *rtmp;
flv_tag_t **video_tags;
size_t video_tag_size;
flv_tag_t *metadata_tag;
byte message[10 * 1024 * 1024];

int main(int argc, char *argv[]) {
  open_flv();
  open_rtmp();

  // send_metadata_packet();
  send_metadata();

  while (!RTMP_ctrlC) {
    static int i = 0;
    send_video_tag(i++);

    i = i % video_tag_size; // loop video_tags
    usleep(1000 / 25 * 1000); // fps: 25
  }

  return die();
}

static void sigIntHandler(int sig) {
  RTMP_ctrlC = TRUE;
  signal(SIGINT, SIG_IGN);
}

void open_flv() {
  RTMP_LogSetLevel(RTMP_LOGINFO);
  infile = fopen("out.flv", "rb");
  get_metadata_tag();
  get_video_tags();
}

void open_rtmp() {
  char *url = "rtmp://shgbit.xyz/app/1";

  rtmp = RTMP_Alloc();
  RTMP_Init(rtmp);
  if (RTMP_SetupURL(rtmp, url) < 0) {
    RTMP_Log(RTMP_LOGERROR, "RTMP_SetupURL FAILED: %s", url);
    die();
  }

  RTMP_EnableWrite(rtmp);

  if (RTMP_Connect(rtmp, NULL) < 0) {
    RTMP_Log(RTMP_LOGERROR, "Connect FAILED: %s", url);
    die();
  }

  if (RTMP_ConnectStream(rtmp, 0) < 0) {
    RTMP_Log(RTMP_LOGERROR, "ConnectStream FAILED");
    die();
  }

  signal(SIGINT, sigIntHandler);
}

int die() {
  RTMP_Close(rtmp);
  RTMP_Free(rtmp);
  fclose(infile);
  exit(0);
  return 0;
}

// 需要在flv metadata body前增加AMF0 String '@setDataFrame' (16 bytes)
void send_metadata_packet() {
  RTMPPacket packet;
  RTMPPacket_Reset(&packet);

  packet.m_packetType = RTMP_PACKET_TYPE_INFO;
  packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
  packet.m_nInfoField2 = rtmp->m_stream_id;
  packet.m_nChannel = 4;
  packet.m_nBodySize += metadata_tag->data_size + 16;
  RTMPPacket_Alloc(&packet, packet.m_nBodySize);

  AVal str = AVC("@setDataFrame");
  assert(AMF_EncodeString(packet.m_body, packet.m_body + packet.m_nBodySize, &str) != NULL);
  fseek(infile, metadata_tag->data_offset, SEEK_SET);
  fread(packet.m_body + 16, 1, metadata_tag->data_size, infile);

  RTMP_LogHexString(RTMP_LOGINFO, (uint8_t *) packet.m_body, packet.m_nBodySize);
  RTMP_SendPacket(rtmp, &packet, false);
  RTMPPacket_Free(&packet);
}

void send_metadata() {
  byte buffer[1024];
  fseek(infile, metadata_tag->offset, SEEK_SET);
  fread(buffer, 1, metadata_tag->size, infile);
  int count = RTMP_Write(rtmp, (char *) buffer, metadata_tag->size);

  RTMP_Log(RTMP_LOGINFO, "send metadata: %d", count);
}

void send_video_tag(uint32_t index) {
  flv_tag_t *current;
  current = video_tags[index];
  fseek(infile, current->offset, SEEK_SET);
  fread(message, 1, current->size, infile);
  int count = RTMP_Write(rtmp, (char *) message, current->size);
  RTMP_Log(RTMP_LOGINFO, "send video tag (#%d): %d", index, count);
}

void get_metadata_tag() {
  flv_tag_t *tag = malloc(sizeof(flv_tag_t));
  tag->offset = 13; // 9: flv_header 4: prev
  fseek(infile, tag->offset, SEEK_SET);
  fread(tag->head, 1, 11, infile);
  tag->type = *tag->head;

  read_ui24((uint32_t *) &(tag->data_size), tag->head + 1);
  tag->data_offset = tag->offset + 11;
  tag->size = tag->data_size + 11;

  RTMP_Log(RTMP_LOGDEBUG, "%s", flv_tag_types[tag->type]);
  RTMP_LogHex(RTMP_LOGDEBUG, tag->head, sizeof(tag->head));
  RTMP_Log(RTMP_LOGDEBUG, "  tag offset: 0x%08x, tag size: %lu", tag->offset, tag->size);
  RTMP_Log(RTMP_LOGDEBUG, "  data offset: 0x%08x, data size: %lu", tag->data_offset, tag->data_size);

  char *obj_buffer = malloc(tag->data_size);
  fread(obj_buffer, 1, tag->data_size, infile);

  AMFObject obj;
  AMF_Decode(&obj, obj_buffer, tag->data_size, false);
  DumpMetaData(&obj);

  metadata_tag = tag;
}

void get_video_tags() {
  video_tags = malloc(sizeof(flv_tag_t *) * 4096);
  flv_tag_t *current;

  // move to first tag address
  fseek(infile, 13, SEEK_SET);
  while ((current = read_tag()) != NULL) {
    if (TAGTYPE_VIDEODATA == current->type) {
      video_tags[video_tag_size++] = current;
    }
  }
}

flv_tag_t *read_tag() {
  static uint32_t _offset = 13;

  flv_tag_t *tag = malloc(sizeof(flv_tag_t));
  tag->offset = _offset;

  if (11 != fread(tag->head, 1, 11, infile)) return NULL;
  tag->type = *tag->head;

  read_ui24((uint32_t *) &(tag->data_size), tag->head + 1);
  tag->data_offset = tag->offset + 11;
  tag->size = tag->data_size + 11;

  RTMP_Log(RTMP_LOGDEBUG, "%s", flv_tag_types[tag->type]);
  RTMP_LogHex(RTMP_LOGDEBUG, tag->head, sizeof(tag->head));
  RTMP_Log(RTMP_LOGDEBUG, "  tag offset: 0x%08x, tag size: %lu", tag->offset, tag->size);
  RTMP_Log(RTMP_LOGDEBUG, "  data offset: 0x%08x, data size: %lu", tag->data_offset, tag->data_size);

  // update _offset
  _offset += 11 + tag->data_size + 4;

  if (0 != fseek(infile, tag->data_size + 4, SEEK_CUR)) return NULL;

  return tag;
}

void read_ui24(uint32_t *out, void *ptr) {
  uint8_t *bytes = (uint8_t *) ptr;
  *out = (bytes[0] << 16) | (bytes[1] << 8) | bytes[2];
}

// from amf.c
static int DumpMetaData(AMFObject *obj) {
  AMFObjectProperty *prop;
  int n, len;
  for (n = 0; n < obj->o_num; n++) {
    char str[256] = "";
    prop = AMF_GetProp(obj, NULL, n);
    switch (prop->p_type) {
    case AMF_OBJECT:
    case AMF_ECMA_ARRAY:
    case AMF_STRICT_ARRAY:
      if (prop->p_name.av_len) RTMP_Log(RTMP_LOGINFO, "%.*s:", prop->p_name.av_len, prop->p_name.av_val);
      DumpMetaData(&prop->p_vu.p_object);
      break;
    case AMF_NUMBER:
      snprintf(str, 255, "%.2f", prop->p_vu.p_number);
      break;
    case AMF_BOOLEAN:
      snprintf(str, 255, "%s", prop->p_vu.p_number != 0. ? "TRUE" : "FALSE");
      break;
    case AMF_STRING:
      len = snprintf(str, 255, "%.*s", prop->p_vu.p_aval.av_len, prop->p_vu.p_aval.av_val);
      if (len >= 1 && str[len - 1] == '\n') str[len - 1] = '\0';
      break;
    case AMF_DATE:
      snprintf(str, 255, "timestamp:%.2f", prop->p_vu.p_number);
      break;
    default:
      snprintf(str, 255, "INVALID TYPE 0x%02x", (unsigned char) prop->p_type);
    }
    if (str[0] && prop->p_name.av_len) {
      RTMP_Log(RTMP_LOGDEBUG, "  %-22.*s%s", prop->p_name.av_len, prop->p_name.av_val, str);
    }
  }
  return FALSE;
}

说明:

  • 理解rtmp和flv关系后,写这个程序就很简单了
    • 第一步: 读取flv文件,记录metadata和video tags
    • 第二步: 初始化rtmp,通过librtmp连接并建立stream
    • 第三步: 发送metadata message
    • 第四步: 依次发送video message
  • flv的metadata是播放过程中存下来的,而如果做为推流来说,则需要增加一个@setDataFormat的AMFString
  • RTMP_Write和RTMP_SendPacket
    • RTMP_SendPacket属于标准构造Packet方法, RTMP_Write属于sugar,简化了flv tag发送,能够针对metadata自动添加@setDataFromat AMFString,如果是从VideoToolbox或者h.264 file过来都需要自己构造RTMPPacket

RTMPPacket的范围包括了Chunk和Message两部分,

typedef struct RTMPPacket {
  uint8_t m_headerType;
  uint8_t m_packetType;
  uint8_t m_hasAbsTimestamp; /* timestamp absolute or relative? */
  int m_nChannel;
  uint32_t m_nTimeStamp; /* timestamp */
  int32_t m_nInfoField2; /* last 4 bytes in a long header */
  uint32_t m_nBodySize;
  uint32_t m_nBytesRead;
  RTMPChunk *m_chunk;
  char *m_body;
} RTMPPacket;

对应的内容如下:

  • m_headerType
    • Type 0: RTMP_PACKET_SIZE_LARGE 0
    • Type 1: RTMP_PACKET_SIZE_MEDIUM 1
    • Type 2: RTMP_PACKET_SIZE_SMALL 2
    • Type 3: RTMP_PACKET_SIZE_MINIMUM 3
  • m_packetType
    • Message Type: audio(8), video(9), data(18) etc.
  • m_nChannel: Chunk Stream ID
  • m_nInfoField2: last 4 bytes in a long header, StreamID
  • m_body: Message body
  • m_nBodySize: Message body size

一般使用来说:

RTMPPacket packet;
RTMPPacket_Reset(&packet);

packet.m_packetType = RTMP_PACKET_TYPE_INFO;
packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
packet.m_nInfoField2 = rtmp->m_stream_id;
packet.m_nChannel = 4;
packet.m_nBodySize += metadata_tag->data_size + 16;
RTMPPacket_Alloc(&packet, packet.m_nBodySize);

AVal str = AVC("@setDataFrame");
assert(AMF_EncodeString(packet.m_body, packet.m_body + packet.m_nBodySize, &str) != NULL);
fseek(infile, metadata_tag->data_offset, SEEK_SET);
fread(packet.m_body + 16, 1, metadata_tag->data_size, infile);

RTMP_LogHexString(RTMP_LOGINFO, (uint8_t *) packet.m_body, packet.m_nBodySize);
RTMP_SendPacket(rtmp, &packet, false);
RTMPPacket_Free(&packet);

nonocast avatar May 20 '22 01:05 nonocast