본문 바로가기
놀기/잡스러운 것

[bmNVR] 수신한 데이터를 받을 Source 만들기 (ByteStreamMemoryBufferSource 참조)

by Hi~ 2023. 9. 1.

앞서 테스트 프로그램으로 사용한 ts 파일을 만드는 프로그램 소스는 간단하다. RTSPClient 테스트 프로그램으로 저장한 H.264 파일을 넣으면 ts 파일을 출력해 준다. 소스도 손바닥 정도로 짤막하다. 대신, 파일을 소스로 하여 ByteStreamFileSource를 사용한다. 우리는 파일이 아닌 RTSPClient로 받은 Stream Data를 바로 전달해야 하므로 적합하지는 않다. 십여 년 전 dshow를 사용할 때는 Shared Memory를 사용하여 Sink / Source filter를 만들어 사용했는데 LIVE555에는 ByteStreamMemoryBufferSource를 제공한다. 이 클래스를 사용하면 간단하게 해결 가능할 것 같다.

 

testH264VideoToTransportStream 소스코드는 아래와 같이 짤막하다.

int main(int argc, char** argv) {
  // Begin by setting up our usage environment:
  TaskScheduler* scheduler = BasicTaskScheduler::createNew();
  env = BasicUsageEnvironment::createNew(*scheduler);

  // Open the input file as a 'byte-stream file source':
  FramedSource* inputSource = ByteStreamFileSource::createNew(*env, inputFileName);
  if (inputSource == NULL) {
    *env << "Unable to open file \"" << inputFileName
	 << "\" as a byte-stream file source\n";
    exit(1);
  }

  // Create a 'framer' filter for this file source, to generate presentation times for each NAL unit:
  H264VideoStreamFramer* framer = H264VideoStreamFramer::createNew(*env, inputSource, True/*includeStartCodeInOutput*/);

  // Then create a filter that packs the H.264 video data into a Transport Stream:
  MPEG2TransportStreamFromESSource* tsFrames = MPEG2TransportStreamFromESSource::createNew(*env);
  tsFrames->addNewVideoSource(framer, 5/*mpegVersion: H.264*/);
  
  // Open the output file as a 'file sink':
  MediaSink* outputSink = FileSink::createNew(*env, outputFileName);
  if (outputSink == NULL) {
    *env << "Unable to open file \"" << outputFileName << "\" as a file sink\n";
    exit(1);
  }

  // Finally, start playing:←←
  *env << "Beginning to read...\n";
  outputSink->startPlaying(*tsFrames, afterPlaying, NULL);

  env->taskScheduler().doEventLoop(); // does not return

  return 0; // only to prevent compiler warning
}

void afterPlaying(void* /*clientData*/) {
  *env << "Done reading.\n";
  *env << "Wrote output file: \"" << outputFileName << "\"\n";
  exit(0);
}

 

이 소스코드에서 ByteStreamFileSource를 ByteStreamMemoryBufferSource와 같은 Source로 변경해야 하는데, 아쉽게 예제 디렉터리에는 없다. 인터넷을 찾아보면 예제가 많이 있겠지만 오랜만에 직접 해보는 재미(?)를 느껴보자.

 

ByteStreamMemoryBufferSource class 소스코드도 길지는 않다. 많은 부분이 상속되면서 숨어 있겠지.

 

ByteStreamMemoryBufferSource에서 우리가 공략해야 할 부분은 doGetNextFrame() 함수다. 지정된 크기의 버퍼에서 꺼내 오는 것이기 때문에 bmNVR에 딱 맞지는 않다. bmNVR은 실시간으로 영상이 들어오는 구조이므로 수정을 해야 한다. 일단, 아래 코드를 각자 천천히 보자. 데이터를 fTo 변수에 넣어주고 fFrameSize에 복사한 크기에 대한 정보를 넣는다. 그리고 fPresentationTime을 설정한다.

 

void ByteStreamMemoryBufferSource::doGetNextFrame() {
  if (fCurIndex >= fBufferSize || (fLimitNumBytesToStream && fNumBytesToStream == 0)) {
    handleClosure();
    return;
  }

  // Try to read as many bytes as will fit in the buffer provided (or "fPreferredFrameSize" if less)
  fFrameSize = fMaxSize;
  if (fLimitNumBytesToStream && fNumBytesToStream < (u_int64_t)fFrameSize) {
    fFrameSize = (unsigned)fNumBytesToStream;
  }
  if (fPreferredFrameSize > 0 && fPreferredFrameSize < fFrameSize) {
    fFrameSize = fPreferredFrameSize;
  }

  if (fCurIndex + fFrameSize > fBufferSize) {
    fFrameSize = (unsigned)(fBufferSize - fCurIndex);
  }

  memmove(fTo, &fBuffer[fCurIndex], fFrameSize);
  fCurIndex += fFrameSize;
  fNumBytesToStream -= fFrameSize;

  // Set the 'presentation time':
  if (fPlayTimePerFrame > 0 && fPreferredFrameSize > 0) {
    if (fPresentationTime.tv_sec == 0 && fPresentationTime.tv_usec == 0) {
      // This is the first frame, so use the current time:
      gettimeofday(&fPresentationTime, NULL);
    } else {
      // Increment by the play time of the previous data:
      unsigned uSeconds	= fPresentationTime.tv_usec + fLastPlayTime;
      fPresentationTime.tv_sec += uSeconds/1000000;
      fPresentationTime.tv_usec = uSeconds%1000000;
    }

    // Remember the play time of this data:
    fLastPlayTime = (fPlayTimePerFrame*fFrameSize)/fPreferredFrameSize;
    fDurationInMicroseconds = fLastPlayTime;
  } else {
    // We don't know a specific play time duration for this data,
    // so just record the current time as being the 'presentation time':
    gettimeofday(&fPresentationTime, NULL);
  }

  // Inform the downstream object that it has data:
  FramedSource::afterGetting(this);
}

 

이 부분을 bmNVR에 맞게 정리하면 아래와 같이 만들 수 있다. 넣을 데이터가 없을 때는 while loop를 좀 돌다가 데이터가 들어오면 fTo에 복사한다. 여기서 주의할 것은 무작정 넣으면 안 되고 버퍼의 남은 공간을 확인하며 넣어야 한다. fMaxSize 값이 넣을 수 있는 최댓값이다. 넣을 데이터는 최대 fMaxSize가 되므로 주의하자. fPresentationTime은 데이터가 실시간으로 들어오니 gettimeofday() 함수를 사용하여 설정한다.

 

void bmNVRStreamBufferSource::doGetNextFrame() {
    while (fS->m_frameDataSize < fPreferredFrameSize)
    {
        usleep(10000);
    }

    gettimeofday(&fPresentationTime, NULL);
    fFrameSize = (fMaxSize > fS->m_frameDataSize) ? fS->m_frameDataSize : fMaxSize;
    fS->m_frameDataSize -= fFrameSize;

    memmove(fTo, fS->m_frameData.constData(), fFrameSize);
    fS->m_frameData.remove(0, fFrameSize);
    //printf("[%5d][%d/%d]\n", idx++, fFrameSize, fS->m_frameDataSize);
    //printf("[%5d][%d/%d] - [%02X][%02X][%02X][%02X][%02X][%02X][%02X][%02X]\n", idx++, fFrameSize, fS->m_frameDataSize
    //       , fTo[0], fTo[1], fTo[2], fTo[3], fTo[4], fTo[5], fTo[6], fTo[7]);

    // Inform the downstream object that it has data:
    FramedSource::afterGetting(this);
}

 

여기서 중요한 것은 bmNVRStreamBufferSource로 데이터를 넘겨주는 방식이다. 다양한 방법이 있겠지만, QT를 사용하는 만큼 signal/slot 방식을 사용하려 한다. 간단하게, bmNVRSink에서 아래와 같이 데이터를 넣어 보내고 bmNVRTransportStream에서 데이터를 받는 간단한 방법으로 했다.

 

bmNVRSink에서는 아래와 같이 하고

char* buff = new char[sizeof(hdr) + frameSize];
memcpy(buff, hdr, sizeof(hdr));
memcpy(buff+sizeof(hdr), fReceiveBuffer, frameSize);
FrameInfo* pFrame = new FrameInfo(nullptr, 1, buff, sizeof(hdr) + frameSize);

m_sender.send(pFrame);

 

bmNVRTransportStream에서는 다음과 같이 받는다.

void bmNVRTransportStream::slotGetFrame(FrameInfo * d)
{
    if (NULL != d) {
        //qDebug() << "new data : " << d->getSize();
        m_frameData.append(d->getBuff(), d->getSize());
        m_frameDataSize += d->getSize();
        d->deleteBuff();
        d->deleteLater();
    }
}

 

bmNVRStreamBufferSource에서는 bmNVRTransportStream의 포인터를 통해 m_frameData와 m_frameDataSize에 접근하는 지저분한 방식을 사용했다. 이 부분은 추후에 한 번 정리해야 겠다. 일단 동작하는 것에 관심이 있으니깐 넘어간다.

 

일단, 이렇게 하면 RTSP 스트림을 받고 저장하는 것까지 완료했다.

댓글