obs/Source/OBSVideoCapture.cpp
jp9000 e04b481c52 Fix potential sync issue with hardware encoders
Instead of the video just buffer using the 'buffering time', make it
properly interleave and wait for audio data to come in before it sends
out data.
2014-05-11 15:45:02 -07:00

1335 lines
45 KiB
C++

/********************************************************************************
Copyright (C) 2012 Hugh Bailey <obs.jim@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
********************************************************************************/
#include "Main.h"
#include <inttypes.h>
#include "mfxstructures.h"
extern "C"
{
#include "../x264/x264.h"
}
#include <memory>
void Convert444toI420(LPBYTE input, int width, int pitch, int height, int startY, int endY, LPBYTE *output);
void Convert444toNV12(LPBYTE input, int width, int inPitch, int outPitch, int height, int startY, int endY, LPBYTE *output);
DWORD STDCALL OBS::EncodeThread(LPVOID lpUnused)
{
App->EncodeLoop();
return 0;
}
DWORD STDCALL OBS::MainCaptureThread(LPVOID lpUnused)
{
App->MainCaptureLoop();
return 0;
}
struct Convert444Data
{
LPBYTE input;
LPBYTE output[3];
bool bNV12;
bool bKillThread;
HANDLE hSignalConvert, hSignalComplete;
int width, height, inPitch, outPitch, startY, endY;
DWORD numThreads;
};
DWORD STDCALL Convert444Thread(Convert444Data *data)
{
do
{
WaitForSingleObject(data->hSignalConvert, INFINITE);
if(data->bKillThread) break;
profileParallelSegment("Convert444Thread", "Convert444Threads", data->numThreads);
if(data->bNV12)
Convert444toNV12(data->input, data->width, data->inPitch, data->outPitch, data->height, data->startY, data->endY, data->output);
else
Convert444toNV12(data->input, data->width, data->inPitch, data->width, data->height, data->startY, data->endY, data->output);
SetEvent(data->hSignalComplete);
}while(!data->bKillThread);
return 0;
}
bool OBS::BufferVideoData(const List<DataPacket> &inputPackets, const List<PacketType> &inputTypes, DWORD timestamp, VideoSegment &segmentOut)
{
VideoSegment &segmentIn = *bufferedVideo.CreateNew();
segmentIn.timestamp = timestamp;
segmentIn.packets.SetSize(inputPackets.Num());
for(UINT i=0; i<inputPackets.Num(); i++)
{
segmentIn.packets[i].data.CopyArray(inputPackets[i].lpPacket, inputPackets[i].size);
segmentIn.packets[i].type = inputTypes[i];
}
bool foundAudio = false;
// wait until there is audio before sending off data -- don't rely on buffering time
OSEnterMutex(hSoundDataMutex);
if (pendingAudioFrames.Num())
{
for (UINT i = 0; i < pendingAudioFrames.Num(); i++)
{
if (pendingAudioFrames[i].timestamp >= bufferedVideo[0].timestamp)
{
foundAudio = true;
break;
}
}
}
OSLeaveMutex(hSoundDataMutex);
if (foundAudio)
{
segmentOut.packets.TransferFrom(bufferedVideo[0].packets);
segmentOut.timestamp = bufferedVideo[0].timestamp;
bufferedVideo.Remove(0);
return true;
}
return false;
}
#define NUM_OUT_BUFFERS 3
struct EncoderPicture
{
x264_picture_t *picOut;
mfxFrameSurface1 *mfxOut;
EncoderPicture() : picOut(nullptr), mfxOut(nullptr) {}
};
bool operator==(const EncoderPicture& lhs, const EncoderPicture& rhs)
{
if(lhs.picOut && rhs.picOut)
return lhs.picOut == rhs.picOut;
if(lhs.mfxOut && rhs.mfxOut)
return lhs.mfxOut == rhs.mfxOut;
return false;
}
struct FrameProcessInfo
{
EncoderPicture *pic;
DWORD frameTimestamp;
QWORD firstFrameTime;
};
void OBS::SendFrame(VideoSegment &curSegment, QWORD firstFrameTime)
{
if(!bSentHeaders)
{
if(network && curSegment.packets[0].data[0] == 0x17) {
network->BeginPublishing();
bSentHeaders = true;
}
}
OSEnterMutex(hSoundDataMutex);
if(pendingAudioFrames.Num())
{
while(pendingAudioFrames.Num())
{
if(firstFrameTime < pendingAudioFrames[0].timestamp)
{
UINT audioTimestamp = UINT(pendingAudioFrames[0].timestamp-firstFrameTime);
//stop sending audio packets when we reach an audio timestamp greater than the video timestamp
if(audioTimestamp > curSegment.timestamp)
break;
if(audioTimestamp == 0 || audioTimestamp > lastAudioTimestamp)
{
List<BYTE> &audioData = pendingAudioFrames[0].audioData;
if(audioData.Num())
{
//Log(TEXT("a:%u, %llu"), audioTimestamp, frameInfo.firstFrameTime+audioTimestamp);
if(network)
network->SendPacket(audioData.Array(), audioData.Num(), audioTimestamp, PacketType_Audio);
if(fileStream)
fileStream->AddPacket(audioData.Array(), audioData.Num(), audioTimestamp, PacketType_Audio);
audioData.Clear();
lastAudioTimestamp = audioTimestamp;
}
}
}
else
nop();
pendingAudioFrames[0].audioData.Clear();
pendingAudioFrames.Remove(0);
}
}
OSLeaveMutex(hSoundDataMutex);
for(UINT i=0; i<curSegment.packets.Num(); i++)
{
VideoPacketData &packet = curSegment.packets[i];
if(packet.type == PacketType_VideoHighest)
bRequestKeyframe = false;
//Log(TEXT("v:%u, %llu"), curSegment.timestamp, frameInfo.firstFrameTime+curSegment.timestamp);
if(network)
network->SendPacket(packet.data.Array(), packet.data.Num(), curSegment.timestamp, packet.type);
if(fileStream)
fileStream->AddPacket(packet.data.Array(), packet.data.Num(), curSegment.timestamp, packet.type);
}
}
bool OBS::ProcessFrame(FrameProcessInfo &frameInfo)
{
List<DataPacket> videoPackets;
List<PacketType> videoPacketTypes;
//------------------------------------
// encode
bufferedTimes << frameInfo.frameTimestamp;
VideoSegment curSegment;
bool bProcessedFrame, bSendFrame = false;
VOID *picIn;
//profileIn("call to encoder");
if (bShutdownEncodeThread)
picIn = NULL;
else
picIn = frameInfo.pic->picOut ? (LPVOID)frameInfo.pic->picOut : (LPVOID)frameInfo.pic->mfxOut;
videoEncoder->Encode(picIn, videoPackets, videoPacketTypes, bufferedTimes[0]);
bProcessedFrame = (videoPackets.Num() != 0);
//buffer video data before sending out
if(bProcessedFrame)
{
bSendFrame = BufferVideoData(videoPackets, videoPacketTypes, bufferedTimes[0], curSegment);
bufferedTimes.Remove(0);
}
else
nop();
//profileOut;
//------------------------------------
// upload
profileIn("sending stuff out");
//send headers before the first frame if not yet sent
if(bSendFrame)
SendFrame(curSegment, frameInfo.firstFrameTime);
profileOut;
return bProcessedFrame;
}
bool STDCALL SleepToNS(QWORD qwNSTime)
{
QWORD t = GetQPCTimeNS();
if (t >= qwNSTime)
return false;
unsigned int milliseconds = (unsigned int)((qwNSTime - t)/1000000);
if (milliseconds > 1) //also accounts for windows 8 sleep problem
{
//trap suspicious sleeps that should never happen
if (milliseconds > 10000)
{
Log(TEXT("Tried to sleep for %u seconds, that can't be right! Triggering breakpoint."), milliseconds);
DebugBreak();
}
OSSleep(milliseconds);
}
for (;;)
{
t = GetQPCTimeNS();
if (t >= qwNSTime)
return true;
Sleep(1);
}
}
bool STDCALL SleepTo100NS(QWORD qw100NSTime)
{
QWORD t = GetQPCTime100NS();
if (t >= qw100NSTime)
return false;
unsigned int milliseconds = (unsigned int)((qw100NSTime - t)/10000);
if (milliseconds > 1) //also accounts for windows 8 sleep problem
OSSleep(milliseconds);
for (;;)
{
t = GetQPCTime100NS();
if (t >= qw100NSTime)
return true;
Sleep(1);
}
}
#ifdef OBS_TEST_BUILD
#define LOGLONGFRAMESDEFAULT 1
#else
#define LOGLONGFRAMESDEFAULT 0
#endif
void OBS::EncodeLoop()
{
QWORD streamTimeStart = GetQPCTimeNS();
QWORD frameTimeNS = 1000000000/fps;
bool bufferedFrames = true; //to avoid constantly polling number of frames
int numTotalDuplicatedFrames = 0, numTotalFrames = 0, numFramesSkipped = 0;
bufferedTimes.Clear();
bool bUsingQSV = videoEncoder->isQSV();//GlobalConfig->GetInt(TEXT("Video Encoding"), TEXT("UseQSV")) != 0;
QWORD sleepTargetTime = streamTimeStart+frameTimeNS;
latestVideoTime = firstSceneTimestamp = streamTimeStart/1000000;
latestVideoTimeNS = streamTimeStart;
firstFrameTimestamp = 0;
UINT encoderInfo = 0;
QWORD messageTime = 0;
EncoderPicture *lastPic = NULL;
UINT skipThreshold = encoderSkipThreshold*2;
UINT no_sleep_counter = 0;
CircularList<QWORD> bufferedTimes;
while(!bShutdownEncodeThread || (bufferedFrames && !bTestStream)) {
if (!SleepToNS(sleepTargetTime += (frameTimeNS/2)))
no_sleep_counter++;
else
no_sleep_counter = 0;
latestVideoTime = sleepTargetTime/1000000;
latestVideoTimeNS = sleepTargetTime;
if (no_sleep_counter < skipThreshold) {
SetEvent(hVideoEvent);
if (encoderInfo) {
if (messageTime == 0) {
messageTime = latestVideoTime+3000;
} else if (latestVideoTime >= messageTime) {
RemoveStreamInfo(encoderInfo);
encoderInfo = 0;
messageTime = 0;
}
}
} else {
numFramesSkipped++;
if (!encoderInfo)
encoderInfo = AddStreamInfo(Str("EncoderLag"), StreamInfoPriority_Critical);
messageTime = 0;
}
if (!SleepToNS(sleepTargetTime += (frameTimeNS/2)))
no_sleep_counter++;
else
no_sleep_counter = 0;
bufferedTimes << latestVideoTime;
if (curFramePic && firstFrameTimestamp) {
while (bufferedTimes[0] < firstFrameTimestamp)
bufferedTimes.Remove(0);
DWORD curFrameTimestamp = DWORD(bufferedTimes[0] - firstFrameTimestamp);
bufferedTimes.Remove(0);
profileIn("encoder thread frame");
FrameProcessInfo frameInfo;
frameInfo.firstFrameTime = firstFrameTimestamp;
frameInfo.frameTimestamp = curFrameTimestamp;
frameInfo.pic = curFramePic;
if (lastPic == frameInfo.pic)
numTotalDuplicatedFrames++;
if(bUsingQSV)
curFramePic->mfxOut->Data.TimeStamp = curFrameTimestamp;
else
curFramePic->picOut->i_pts = curFrameTimestamp;
ProcessFrame(frameInfo);
if (bShutdownEncodeThread)
bufferedFrames = videoEncoder->HasBufferedFrames();
lastPic = frameInfo.pic;
profileOut;
numTotalFrames++;
}
}
//flush all video frames in the "scene buffering time" buffer
if (firstFrameTimestamp && bufferedVideo.Num())
{
QWORD startTime = GetQPCTimeMS();
DWORD baseTimestamp = bufferedVideo[0].timestamp;
for(UINT i=0; i<bufferedVideo.Num(); i++)
{
//we measure our own time rather than sleep between frames due to potential sleep drift
QWORD curTime;
do
{
curTime = GetQPCTimeMS();
OSSleep (1);
} while (curTime - startTime < bufferedVideo[i].timestamp - baseTimestamp);
SendFrame(bufferedVideo[i], firstFrameTimestamp);
bufferedVideo[i].Clear();
numTotalFrames++;
}
bufferedVideo.Clear();
}
Log(TEXT("Total frames encoded: %d, total frames duplicated: %d (%0.2f%%)"), numTotalFrames, numTotalDuplicatedFrames, (numTotalFrames > 0) ? (double(numTotalDuplicatedFrames)/double(numTotalFrames))*100.0 : 0.0f);
if (numFramesSkipped)
Log(TEXT("Number of frames skipped due to encoder lag: %d (%0.2f%%)"), numFramesSkipped, (numTotalFrames > 0) ? (double(numFramesSkipped)/double(numTotalFrames))*100.0 : 0.0f);
SetEvent(hVideoEvent);
bShutdownVideoThread = true;
}
void OBS::DrawPreview(const Vect2 &renderFrameSize, const Vect2 &renderFrameOffset, const Vect2 &renderFrameCtrlSize, int curRenderTarget, PreviewDrawType type)
{
LoadVertexShader(mainVertexShader);
LoadPixelShader(mainPixelShader);
Ortho(0.0f, renderFrameCtrlSize.x, renderFrameCtrlSize.y, 0.0f, -100.0f, 100.0f);
if(type != Preview_Projector
&& (renderFrameCtrlSize.x != oldRenderFrameCtrlWidth
|| renderFrameCtrlSize.y != oldRenderFrameCtrlHeight))
{
// User is drag resizing the window. We don't recreate the swap chains so our coordinates are wrong
SetViewport(0.0f, 0.0f, (float)oldRenderFrameCtrlWidth, (float)oldRenderFrameCtrlHeight);
}
else
SetViewport(0.0f, 0.0f, renderFrameCtrlSize.x, renderFrameCtrlSize.y);
// Draw background (Black if fullscreen/projector, window colour otherwise)
if(type == Preview_Fullscreen || type == Preview_Projector)
ClearColorBuffer(0x000000);
else
ClearColorBuffer(GetSysColor(COLOR_BTNFACE));
if(bTransitioning)
{
BlendFunction(GS_BLEND_ONE, GS_BLEND_ZERO);
DrawSprite(transitionTexture, 0xFFFFFFFF,
renderFrameOffset.x, renderFrameOffset.y,
renderFrameOffset.x + renderFrameSize.x, renderFrameOffset.y + renderFrameSize.y);
BlendFunction(GS_BLEND_FACTOR, GS_BLEND_INVFACTOR, transitionAlpha);
}
DrawSprite(mainRenderTextures[curRenderTarget], 0xFFFFFFFF,
renderFrameOffset.x, renderFrameOffset.y,
renderFrameOffset.x + renderFrameSize.x, renderFrameOffset.y + renderFrameSize.y);
}
const float yuvFullMat[][16] = {
{0.000000f, 1.000000f, 0.000000f, 0.000000f,
0.000000f, 0.000000f, 1.000000f, 0.000000f,
1.000000f, 0.000000f, 0.000000f, 0.000000f,
0.000000f, 0.000000f, 0.000000f, 1.000000f},
{0.250000f, 0.500000f, 0.250000f, 0.000000f,
-0.249020f, 0.498039f, -0.249020f, 0.501961f,
0.498039f, 0.000000f, -0.498039f, 0.501961f,
0.000000f, 0.000000f, 0.000000f, 1.000000f},
{0.262700f, 0.678000f, 0.059300f, 0.000000f,
-0.139082f, -0.358957f, 0.498039f, 0.501961f,
0.498039f, -0.457983f, -0.040057f, 0.501961f,
0.000000f, 0.000000f, 0.000000f, 1.000000f},
{0.212600f, 0.715200f, 0.072200f, 0.000000f,
-0.114123f, -0.383916f, 0.498039f, 0.501961f,
0.498039f, -0.452372f, -0.045667f, 0.501961f,
0.000000f, 0.000000f, 0.000000f, 1.000000f},
{0.212200f, 0.701300f, 0.086500f, 0.000000f,
-0.115691f, -0.382348f, 0.498039f, 0.501961f,
0.498039f, -0.443355f, -0.054684f, 0.501961f,
0.000000f, 0.000000f, 0.000000f, 1.000000f},
{0.299000f, 0.587000f, 0.114000f, 0.000000f,
-0.168074f, -0.329965f, 0.498039f, 0.501961f,
0.498039f, -0.417046f, -0.080994f, 0.501961f,
0.000000f, 0.000000f, 0.000000f, 1.000000f},
};
const float yuvMat[][16] = {
{0.000000f, 0.858824f, 0.000000f, 0.062745f,
0.000000f, 0.000000f, 0.858824f, 0.062745f,
0.858824f, 0.000000f, 0.000000f, 0.062745f,
0.000000f, 0.000000f, 0.000000f, 1.000000f},
{0.214706f, 0.429412f, 0.214706f, 0.062745f,
-0.219608f, 0.439216f, -0.219608f, 0.501961f,
0.439216f, 0.000000f, -0.439216f, 0.501961f,
0.000000f, 0.000000f, 0.000000f, 1.000000f},
{0.225613f, 0.582282f, 0.050928f, 0.062745f,
-0.122655f, -0.316560f, 0.439216f, 0.501961f,
0.439216f, -0.403890f, -0.035325f, 0.501961f,
0.000000f, 0.000000f, 0.000000f, 1.000000f},
{0.182586f, 0.614231f, 0.062007f, 0.062745f,
-0.100644f, -0.338572f, 0.439216f, 0.501961f,
0.439216f, -0.398942f, -0.040274f, 0.501961f,
0.000000f, 0.000000f, 0.000000f, 1.000000f},
{0.182242f, 0.602293f, 0.074288f, 0.062745f,
-0.102027f, -0.337189f, 0.439216f, 0.501961f,
0.439216f, -0.390990f, -0.048226f, 0.501961f,
0.000000f, 0.000000f, 0.000000f, 1.000000f},
{0.256788f, 0.504129f, 0.097906f, 0.062745f,
-0.148223f, -0.290993f, 0.439216f, 0.501961f,
0.439216f, -0.367788f, -0.071427f, 0.501961f,
0.000000f, 0.000000f, 0.000000f, 1.000000f},
};
//todo: this function is an abomination, this is just disgusting. fix it.
//...seriously, this is really, really horrible. I mean this is amazingly bad.
void OBS::MainCaptureLoop()
{
int curRenderTarget = 0, curYUVTexture = 0, curCopyTexture = 0;
int copyWait = NUM_RENDER_BUFFERS-1;
bSentHeaders = false;
bFirstAudioPacket = true;
bool bLogLongFramesProfile = GlobalConfig->GetInt(TEXT("General"), TEXT("LogLongFramesProfile"), LOGLONGFRAMESDEFAULT) != 0;
float logLongFramesProfilePercentage = GlobalConfig->GetFloat(TEXT("General"), TEXT("LogLongFramesProfilePercentage"), 10.f);
Vect2 baseSize = Vect2(float(baseCX), float(baseCY));
Vect2 outputSize = Vect2(float(outputCX), float(outputCY));
Vect2 scaleSize = Vect2(float(scaleCX), float(scaleCY));
HANDLE hMatrix = yuvScalePixelShader->GetParameterByName(TEXT("yuvMat"));
HANDLE hScaleVal = yuvScalePixelShader->GetParameterByName(TEXT("baseDimensionI"));
//----------------------------------------
// x264 input buffers
int curOutBuffer = 0;
bool bUsingQSV = videoEncoder->isQSV();//GlobalConfig->GetInt(TEXT("Video Encoding"), TEXT("UseQSV")) != 0;
bUsing444 = false;
EncoderPicture lastPic;
EncoderPicture outPics[NUM_OUT_BUFFERS];
for(int i=0; i<NUM_OUT_BUFFERS; i++)
{
if(bUsingQSV)
{
outPics[i].mfxOut = new mfxFrameSurface1;
memset(outPics[i].mfxOut, 0, sizeof(mfxFrameSurface1));
mfxFrameData& data = outPics[i].mfxOut->Data;
videoEncoder->RequestBuffers(&data);
}
else
{
outPics[i].picOut = new x264_picture_t;
x264_picture_init(outPics[i].picOut);
}
}
if(bUsing444)
{
for(int i=0; i<NUM_OUT_BUFFERS; i++)
{
outPics[i].picOut->img.i_csp = X264_CSP_BGRA; //although the x264 input says BGR, x264 actually will expect packed UYV
outPics[i].picOut->img.i_plane = 1;
}
}
else
{
if(!bUsingQSV)
for(int i=0; i<NUM_OUT_BUFFERS; i++)
x264_picture_alloc(outPics[i].picOut, X264_CSP_NV12, outputCX, outputCY);
}
int bCongestionControl = AppConfig->GetInt (TEXT("Video Encoding"), TEXT("CongestionControl"), 0);
bool bDynamicBitrateSupported = App->GetVideoEncoder()->DynamicBitrateSupported();
int defaultBitRate = AppConfig->GetInt(TEXT("Video Encoding"), TEXT("MaxBitrate"), 1000);
int currentBitRate = defaultBitRate;
QWORD lastAdjustmentTime = 0;
UINT adjustmentStreamId = 0;
//std::unique_ptr<ProfilerNode> encodeThreadProfiler;
//----------------------------------------
// time/timestamp stuff
bool bWasLaggedFrame = false;
totalStreamTime = 0;
lastAudioTimestamp = 0;
//----------------------------------------
// start audio capture streams
desktopAudio->StartCapture();
if(micAudio) micAudio->StartCapture();
//----------------------------------------
// status bar/statistics stuff
DWORD fpsCounter = 0;
int numLongFrames = 0;
int numTotalFrames = 0;
bytesPerSec = 0;
captureFPS = 0;
curFramesDropped = 0;
curStrain = 0.0;
PostMessage(hwndMain, OBS_UPDATESTATUSBAR, 0, 0);
QWORD lastBytesSent[3] = {0, 0, 0};
DWORD lastFramesDropped = 0;
double bpsTime = 0.0;
double lastStrain = 0.0f;
DWORD numSecondsWaited = 0;
//----------------------------------------
// 444->420 thread data
int numThreads = MAX(OSGetTotalCores()-2, 1);
HANDLE *h420Threads = (HANDLE*)Allocate(sizeof(HANDLE)*numThreads);
Convert444Data *convertInfo = (Convert444Data*)Allocate(sizeof(Convert444Data)*numThreads);
zero(h420Threads, sizeof(HANDLE)*numThreads);
zero(convertInfo, sizeof(Convert444Data)*numThreads);
for(int i=0; i<numThreads; i++)
{
convertInfo[i].width = outputCX;
convertInfo[i].height = outputCY;
convertInfo[i].hSignalConvert = CreateEvent(NULL, FALSE, FALSE, NULL);
convertInfo[i].hSignalComplete = CreateEvent(NULL, FALSE, FALSE, NULL);
convertInfo[i].bNV12 = bUsingQSV;
convertInfo[i].numThreads = numThreads;
if(i == 0)
convertInfo[i].startY = 0;
else
convertInfo[i].startY = convertInfo[i-1].endY;
if(i == (numThreads-1))
convertInfo[i].endY = outputCY;
else
convertInfo[i].endY = ((outputCY/numThreads)*(i+1)) & 0xFFFFFFFE;
}
bool bEncode;
bool bFirstFrame = true;
bool bFirstImage = true;
bool bFirstEncode = true;
bool bUseThreaded420 = bUseMultithreadedOptimizations && (OSGetTotalCores() > 1) && !bUsing444;
List<HANDLE> completeEvents;
if(bUseThreaded420)
{
for(int i=0; i<numThreads; i++)
{
h420Threads[i] = OSCreateThread((XTHREAD)Convert444Thread, convertInfo+i);
completeEvents << convertInfo[i].hSignalComplete;
}
}
//----------------------------------------
QWORD streamTimeStart = GetQPCTimeNS();
QWORD lastStreamTime = 0;
QWORD firstFrameTimeMS = streamTimeStart/1000000;
QWORD frameLengthNS = 1000000000/fps;
while(WaitForSingleObject(hVideoEvent, INFINITE) == WAIT_OBJECT_0)
{
if (bShutdownVideoThread)
break;
QWORD renderStartTime = GetQPCTimeNS();
totalStreamTime = DWORD((renderStartTime-streamTimeStart)/1000000);
bool bRenderView = !IsIconic(hwndMain) && bRenderViewEnabled;
QWORD renderStartTimeMS = renderStartTime/1000000;
QWORD curStreamTime = latestVideoTimeNS;
if (!lastStreamTime)
lastStreamTime = curStreamTime-frameLengthNS;
QWORD frameDelta = curStreamTime-lastStreamTime;
//if (!lastStreamTime)
// lastStreamTime = renderStartTime-frameLengthNS;
//QWORD frameDelta = renderStartTime-lastStreamTime;
double fSeconds = double(frameDelta)*0.000000001;
//lastStreamTime = renderStartTime;
bool bUpdateBPS = false;
profileIn("video thread frame");
//Log(TEXT("Stream Time: %llu"), curStreamTime);
//Log(TEXT("frameDelta: %lf"), fSeconds);
//------------------------------------
if(bRequestKeyframe && keyframeWait > 0)
{
keyframeWait -= int(frameDelta);
if(keyframeWait <= 0)
{
GetVideoEncoder()->RequestKeyframe();
bRequestKeyframe = false;
}
}
if(!pushToTalkDown && pushToTalkTimeLeft > 0)
{
pushToTalkTimeLeft -= int(frameDelta);
OSDebugOut(TEXT("time left: %d\r\n"), pushToTalkTimeLeft);
if(pushToTalkTimeLeft <= 0)
{
pushToTalkTimeLeft = 0;
bPushToTalkOn = false;
}
}
//------------------------------------
OSEnterMutex(hSceneMutex);
if (bPleaseEnableProjector)
ActuallyEnableProjector();
else if(bPleaseDisableProjector)
DisableProjector();
if(bResizeRenderView)
{
GS->ResizeView();
bResizeRenderView = false;
}
//------------------------------------
if(scene)
{
profileIn("scene->Preprocess");
scene->Preprocess();
for(UINT i=0; i<globalSources.Num(); i++)
globalSources[i].source->Preprocess();
profileOut;
scene->Tick(float(fSeconds));
for(UINT i=0; i<globalSources.Num(); i++)
globalSources[i].source->Tick(float(fSeconds));
}
//------------------------------------
QWORD curBytesSent = 0;
if (network) {
curBytesSent = network->GetCurrentSentBytes();
curFramesDropped = network->NumDroppedFrames();
} else if (numSecondsWaited) {
//reset stats if the network disappears
bytesPerSec = 0;
bpsTime = 0;
numSecondsWaited = 0;
curBytesSent = 0;
zero(lastBytesSent, sizeof(lastBytesSent));
}
bpsTime += fSeconds;
if(bpsTime > 1.0f)
{
if(numSecondsWaited < 3)
++numSecondsWaited;
//bytesPerSec = DWORD(curBytesSent - lastBytesSent);
bytesPerSec = DWORD(curBytesSent - lastBytesSent[0]) / numSecondsWaited;
if(bpsTime > 2.0)
bpsTime = 0.0f;
else
bpsTime -= 1.0;
if(numSecondsWaited == 3)
{
lastBytesSent[0] = lastBytesSent[1];
lastBytesSent[1] = lastBytesSent[2];
lastBytesSent[2] = curBytesSent;
}
else
lastBytesSent[numSecondsWaited] = curBytesSent;
captureFPS = fpsCounter;
fpsCounter = 0;
bUpdateBPS = true;
}
fpsCounter++;
if(network) curStrain = network->GetPacketStrain();
EnableBlending(TRUE);
BlendFunction(GS_BLEND_SRCALPHA, GS_BLEND_INVSRCALPHA);
//------------------------------------
// render the mini render texture
LoadVertexShader(mainVertexShader);
LoadPixelShader(mainPixelShader);
SetRenderTarget(mainRenderTextures[curRenderTarget]);
Ortho(0.0f, baseSize.x, baseSize.y, 0.0f, -100.0f, 100.0f);
SetViewport(0, 0, baseSize.x, baseSize.y);
if(scene)
scene->Render();
//------------------------------------
if(bTransitioning)
{
if(!transitionTexture)
{
transitionTexture = CreateTexture(baseCX, baseCY, GS_BGRA, NULL, FALSE, TRUE);
if(transitionTexture)
{
D3D10Texture *d3dTransitionTex = static_cast<D3D10Texture*>(transitionTexture);
D3D10Texture *d3dSceneTex = static_cast<D3D10Texture*>(mainRenderTextures[lastRenderTarget]);
GetD3D()->CopyResource(d3dTransitionTex->texture, d3dSceneTex->texture);
}
else
bTransitioning = false;
}
else if(transitionAlpha >= 1.0f)
{
delete transitionTexture;
transitionTexture = NULL;
bTransitioning = false;
}
}
if(bTransitioning)
{
EnableBlending(TRUE);
transitionAlpha += float(fSeconds)*5.0f;
if(transitionAlpha > 1.0f)
transitionAlpha = 1.0f;
}
else
EnableBlending(FALSE);
//------------------------------------
// render the mini view thingy
if (bProjector) {
SetRenderTarget(projectorTexture);
Vect2 renderFrameSize, renderFrameOffset;
Vect2 projectorSize = Vect2(float(projectorWidth), float(projectorHeight));
float projectorAspect = (projectorSize.x / projectorSize.y);
float baseAspect = (baseSize.x / baseSize.y);
if (projectorAspect < baseAspect) {
float fProjectorWidth = float(projectorWidth);
renderFrameSize = Vect2(fProjectorWidth, fProjectorWidth / baseAspect);
renderFrameOffset = Vect2(0.0f, (projectorSize.y-renderFrameSize.y) * 0.5f);
} else {
float fProjectorHeight = float(projectorHeight);
renderFrameSize = Vect2(fProjectorHeight * baseAspect, fProjectorHeight);
renderFrameOffset = Vect2((projectorSize.x-renderFrameSize.x) * 0.5f, 0.0f);
}
DrawPreview(renderFrameSize, renderFrameOffset, projectorSize, curRenderTarget, Preview_Projector);
SetRenderTarget(NULL);
}
if(bRenderView)
{
// Cache
const Vect2 renderFrameSize = GetRenderFrameSize();
const Vect2 renderFrameOffset = GetRenderFrameOffset();
const Vect2 renderFrameCtrlSize = GetRenderFrameControlSize();
SetRenderTarget(NULL);
DrawPreview(renderFrameSize, renderFrameOffset, renderFrameCtrlSize, curRenderTarget,
bFullscreenMode ? Preview_Fullscreen : Preview_Standard);
//draw selections if in edit mode
if(bEditMode && !bSizeChanging)
{
if(scene) {
LoadVertexShader(solidVertexShader);
LoadPixelShader(solidPixelShader);
solidPixelShader->SetColor(solidPixelShader->GetParameter(0), 0xFF0000);
scene->RenderSelections(solidPixelShader);
}
}
}
else if(bForceRenderViewErase)
{
InvalidateRect(hwndRenderFrame, NULL, TRUE);
UpdateWindow(hwndRenderFrame);
bForceRenderViewErase = false;
}
//------------------------------------
// actual stream output
LoadVertexShader(mainVertexShader);
LoadPixelShader(yuvScalePixelShader);
Texture *yuvRenderTexture = yuvRenderTextures[curRenderTarget];
SetRenderTarget(yuvRenderTexture);
switch(colorDesc.matrix)
{
case ColorMatrix_GBR:
yuvScalePixelShader->SetMatrix(hMatrix, colorDesc.fullRange ? (float*)yuvFullMat[0] : (float*)yuvMat[0]);
break;
case ColorMatrix_YCgCo:
yuvScalePixelShader->SetMatrix(hMatrix, colorDesc.fullRange ? (float*)yuvFullMat[1] : (float*)yuvMat[1]);
break;
case ColorMatrix_BT2020NCL:
yuvScalePixelShader->SetMatrix(hMatrix, colorDesc.fullRange ? (float*)yuvFullMat[2] : (float*)yuvMat[2]);
break;
case ColorMatrix_BT709:
yuvScalePixelShader->SetMatrix(hMatrix, colorDesc.fullRange ? (float*)yuvFullMat[3] : (float*)yuvMat[3]);
break;
case ColorMatrix_SMPTE240M:
yuvScalePixelShader->SetMatrix(hMatrix, colorDesc.fullRange ? (float*)yuvFullMat[4] : (float*)yuvMat[4]);
break;
default:
yuvScalePixelShader->SetMatrix(hMatrix, colorDesc.fullRange ? (float*)yuvFullMat[5] : (float*)yuvMat[5]);
}
if(downscale < 2.01)
yuvScalePixelShader->SetVector2(hScaleVal, 1.0f/baseSize);
else if(downscale < 3.01)
yuvScalePixelShader->SetVector2(hScaleVal, 1.0f/(outputSize*3.0f));
Ortho(0.0f, outputSize.x, outputSize.y, 0.0f, -100.0f, 100.0f);
SetViewport(0.0f, 0.0f, outputSize.x, outputSize.y);
//why am I using scaleSize instead of outputSize for the texture?
//because outputSize can be trimmed by up to three pixels due to 128-bit alignment.
//using the scale function with outputSize can cause slightly inaccurate scaled images
if(bTransitioning)
{
BlendFunction(GS_BLEND_ONE, GS_BLEND_ZERO);
DrawSpriteEx(transitionTexture, 0xFFFFFFFF, 0.0f, 0.0f, scaleSize.x, scaleSize.y, 0.0f, 0.0f, 1.0f, 1.0f);
BlendFunction(GS_BLEND_FACTOR, GS_BLEND_INVFACTOR, transitionAlpha);
}
DrawSpriteEx(mainRenderTextures[curRenderTarget], 0xFFFFFFFF, 0.0f, 0.0f, outputSize.x, outputSize.y, 0.0f, 0.0f, 1.0f, 1.0f);
//------------------------------------
if (bProjector && !copyWait)
projectorSwap->Present(0, 0);
if(bRenderView && !copyWait)
static_cast<D3D10System*>(GS)->swap->Present(0, 0);
OSLeaveMutex(hSceneMutex);
//------------------------------------
// present/upload
profileIn("GPU download and conversion");
bEncode = true;
if(copyWait)
{
copyWait--;
bEncode = false;
}
else
{
//audio sometimes takes a bit to start -- do not start processing frames until audio has started capturing
if(!bRecievedFirstAudioFrame)
{
static bool bWarnedAboutNoAudio = false;
if (renderStartTimeMS-firstFrameTimeMS > 10000 && !bWarnedAboutNoAudio)
{
bWarnedAboutNoAudio = true;
//AddStreamInfo (TEXT ("WARNING: OBS is not receiving audio frames. Please check your audio devices."), StreamInfoPriority_Critical);
}
bEncode = false;
}
else if(bFirstFrame)
{
firstFrameTimestamp = lastStreamTime/1000000;
bFirstFrame = false;
}
if(!bEncode)
{
if(curYUVTexture == (NUM_RENDER_BUFFERS-1))
curYUVTexture = 0;
else
curYUVTexture++;
}
}
lastStreamTime = curStreamTime;
if(bEncode)
{
UINT prevCopyTexture = (curCopyTexture == 0) ? NUM_RENDER_BUFFERS-1 : curCopyTexture-1;
ID3D10Texture2D *copyTexture = copyTextures[curCopyTexture];
profileIn("CopyResource");
if(!bFirstEncode && bUseThreaded420)
{
WaitForMultipleObjects(completeEvents.Num(), completeEvents.Array(), TRUE, INFINITE);
copyTexture->Unmap(0);
}
D3D10Texture *d3dYUV = static_cast<D3D10Texture*>(yuvRenderTextures[curYUVTexture]);
GetD3D()->CopyResource(copyTexture, d3dYUV->texture);
profileOut;
ID3D10Texture2D *prevTexture = copyTextures[prevCopyTexture];
if(bFirstImage) //ignore the first frame
bFirstImage = false;
else
{
HRESULT result;
D3D10_MAPPED_TEXTURE2D map;
if(SUCCEEDED(result = prevTexture->Map(0, D3D10_MAP_READ, 0, &map)))
{
int prevOutBuffer = (curOutBuffer == 0) ? NUM_OUT_BUFFERS-1 : curOutBuffer-1;
int nextOutBuffer = (curOutBuffer == NUM_OUT_BUFFERS-1) ? 0 : curOutBuffer+1;
EncoderPicture &prevPicOut = outPics[prevOutBuffer];
EncoderPicture &picOut = outPics[curOutBuffer];
EncoderPicture &nextPicOut = outPics[nextOutBuffer];
if(!bUsing444)
{
profileIn("conversion to 4:2:0");
if(bUseThreaded420)
{
for(int i=0; i<numThreads; i++)
{
convertInfo[i].input = (LPBYTE)map.pData;
convertInfo[i].inPitch = map.RowPitch;
if(bUsingQSV)
{
mfxFrameData& data = nextPicOut.mfxOut->Data;
videoEncoder->RequestBuffers(&data);
convertInfo[i].outPitch = data.Pitch;
convertInfo[i].output[0] = data.Y;
convertInfo[i].output[1] = data.UV;
}
else
{
convertInfo[i].output[0] = nextPicOut.picOut->img.plane[0];
convertInfo[i].output[1] = nextPicOut.picOut->img.plane[1];
convertInfo[i].output[2] = nextPicOut.picOut->img.plane[2];
}
SetEvent(convertInfo[i].hSignalConvert);
}
if(bFirstEncode)
bFirstEncode = bEncode = false;
}
else
{
if(bUsingQSV)
{
mfxFrameData& data = picOut.mfxOut->Data;
videoEncoder->RequestBuffers(&data);
LPBYTE output[] = {data.Y, data.UV};
Convert444toNV12((LPBYTE)map.pData, outputCX, map.RowPitch, data.Pitch, outputCY, 0, outputCY, output);
}
else
Convert444toNV12((LPBYTE)map.pData, outputCX, map.RowPitch, outputCX, outputCY, 0, outputCY, picOut.picOut->img.plane);
prevTexture->Unmap(0);
}
profileOut;
}
if(bEncode)
{
//encodeThreadProfiler.reset(::new ProfilerNode(TEXT("EncodeThread"), true));
//encodeThreadProfiler->MonitorThread(hEncodeThread);
curFramePic = &picOut;
}
curOutBuffer = nextOutBuffer;
}
else
{
//We have to crash, or we end up deadlocking the thread when the convert threads are never signalled
if (result == DXGI_ERROR_DEVICE_REMOVED)
{
String message;
HRESULT reason = GetD3D()->GetDeviceRemovedReason();
switch (reason)
{
case DXGI_ERROR_DEVICE_RESET:
case DXGI_ERROR_DEVICE_HUNG:
message = TEXT("Your video card or driver froze and was reset. Please check for possible hardware / driver issues.");
break;
case DXGI_ERROR_DEVICE_REMOVED:
message = TEXT("Your video card disappeared from the system. Please check for possible hardware / driver issues.");
break;
case DXGI_ERROR_DRIVER_INTERNAL_ERROR:
message = TEXT("Your video driver reported an internal error. Please check for possible hardware / driver issues.");
break;
case DXGI_ERROR_INVALID_CALL:
message = TEXT("Your video driver reported an invalid call. Please check for possible driver issues.");
break;
default:
message = TEXT("DXGI_ERROR_DEVICE_REMOVED");
break;
}
message << TEXT(" This error can also occur if you have enabled opencl in x264 custom settings.");
CrashError (TEXT("Texture->Map failed: 0x%08x 0x%08x\r\n\r\n%s"), result, reason, message.Array());
}
else
CrashError (TEXT("Texture->Map failed: 0x%08x"), result);
}
}
if(curCopyTexture == (NUM_RENDER_BUFFERS-1))
curCopyTexture = 0;
else
curCopyTexture++;
if(curYUVTexture == (NUM_RENDER_BUFFERS-1))
curYUVTexture = 0;
else
curYUVTexture++;
if (bCongestionControl && bDynamicBitrateSupported && !bTestStream && totalStreamTime > 15000)
{
if (curStrain > 25)
{
if (renderStartTimeMS - lastAdjustmentTime > 1500)
{
if (currentBitRate > 100)
{
currentBitRate = (int)(currentBitRate * (1.0 - (curStrain / 400)));
App->GetVideoEncoder()->SetBitRate(currentBitRate, -1);
if (!adjustmentStreamId)
adjustmentStreamId = App->AddStreamInfo (FormattedString(TEXT("Congestion detected, dropping bitrate to %d kbps"), currentBitRate).Array(), StreamInfoPriority_Low);
else
App->SetStreamInfo(adjustmentStreamId, FormattedString(TEXT("Congestion detected, dropping bitrate to %d kbps"), currentBitRate).Array());
bUpdateBPS = true;
}
lastAdjustmentTime = renderStartTimeMS;
}
}
else if (currentBitRate < defaultBitRate && curStrain < 5 && lastStrain < 5)
{
if (renderStartTimeMS - lastAdjustmentTime > 5000)
{
if (currentBitRate < defaultBitRate)
{
currentBitRate += (int)(defaultBitRate * 0.05);
if (currentBitRate > defaultBitRate)
currentBitRate = defaultBitRate;
}
App->GetVideoEncoder()->SetBitRate(currentBitRate, -1);
/*if (!adjustmentStreamId)
App->AddStreamInfo (FormattedString(TEXT("Congestion clearing, raising bitrate to %d kbps"), currentBitRate).Array(), StreamInfoPriority_Low);
else
App->SetStreamInfo(adjustmentStreamId, FormattedString(TEXT("Congestion clearing, raising bitrate to %d kbps"), currentBitRate).Array());*/
App->RemoveStreamInfo(adjustmentStreamId);
adjustmentStreamId = 0;
bUpdateBPS = true;
lastAdjustmentTime = renderStartTimeMS;
}
}
}
}
lastRenderTarget = curRenderTarget;
if(curRenderTarget == (NUM_RENDER_BUFFERS-1))
curRenderTarget = 0;
else
curRenderTarget++;
if(bUpdateBPS || !CloseDouble(curStrain, lastStrain) || curFramesDropped != lastFramesDropped)
{
PostMessage(hwndMain, OBS_UPDATESTATUSBAR, 0, 0);
lastStrain = curStrain;
lastFramesDropped = curFramesDropped;
}
//------------------------------------
// we're about to sleep so we should flush the d3d command queue
profileIn("flush");
GetD3D()->Flush();
profileOut;
profileOut;
profileOut; //frame
//------------------------------------
// frame sync
//QWORD renderStopTime = GetQPCTimeNS();
if(bWasLaggedFrame = (frameDelta > frameLengthNS))
{
numLongFrames++;
if(bLogLongFramesProfile && (numLongFrames/float(max(1, numTotalFrames)) * 100.) > logLongFramesProfilePercentage)
DumpLastProfileData();
}
//OSDebugOut(TEXT("Frame adjust time: %d, "), frameTimeAdjust-totalTime);
numTotalFrames++;
}
DisableProjector();
//encodeThreadProfiler.reset();
if(!bUsing444)
{
if(bUseThreaded420)
{
for(int i=0; i<numThreads; i++)
{
if(h420Threads[i])
{
convertInfo[i].bKillThread = true;
SetEvent(convertInfo[i].hSignalConvert);
OSTerminateThread(h420Threads[i], 10000);
h420Threads[i] = NULL;
}
if(convertInfo[i].hSignalConvert)
{
CloseHandle(convertInfo[i].hSignalConvert);
convertInfo[i].hSignalConvert = NULL;
}
if(convertInfo[i].hSignalComplete)
{
CloseHandle(convertInfo[i].hSignalComplete);
convertInfo[i].hSignalComplete = NULL;
}
}
if(!bFirstEncode)
{
ID3D10Texture2D *copyTexture = copyTextures[curCopyTexture];
copyTexture->Unmap(0);
}
}
if(bUsingQSV)
for(int i = 0; i < NUM_OUT_BUFFERS; i++)
delete outPics[i].mfxOut;
else
for(int i=0; i<NUM_OUT_BUFFERS; i++)
{
x264_picture_clean(outPics[i].picOut);
delete outPics[i].picOut;
}
}
Free(h420Threads);
Free(convertInfo);
Log(TEXT("Total frames rendered: %d, number of late frames: %d (%0.2f%%) (it's okay for some frames to be late)"), numTotalFrames, numLongFrames, (numTotalFrames > 0) ? (double(numLongFrames)/double(numTotalFrames))*100.0 : 0.0f);
}