mirror of
https://github.com/moonlight-stream/moonlight-common-c.git
synced 2025-08-17 17:05:50 +00:00
Add backwards compatibility for GFE 2.1.x
This commit is contained in:
parent
7883ce67cd
commit
0fa1a02e0a
@ -8,6 +8,8 @@ static CONNECTION_LISTENER_CALLBACKS originalCallbacks;
|
||||
// This is used for debug prints so it's not declared static
|
||||
PLATFORM_CALLBACKS platformCallbacks;
|
||||
|
||||
int serverMajorVersion;
|
||||
|
||||
static int alreadyTerminated;
|
||||
|
||||
/* Connection stages */
|
||||
@ -148,9 +150,11 @@ void LiCompleteThreadStart(void)
|
||||
/* Starts the connection to the streaming machine */
|
||||
int LiStartConnection(IP_ADDRESS host, PSTREAM_CONFIGURATION streamConfig, PCONNECTION_LISTENER_CALLBACKS clCallbacks,
|
||||
PDECODER_RENDERER_CALLBACKS drCallbacks, PAUDIO_RENDERER_CALLBACKS arCallbacks, PPLATFORM_CALLBACKS plCallbacks,
|
||||
void* renderContext, int drFlags) {
|
||||
void* renderContext, int drFlags, int _serverMajorVersion) {
|
||||
int err;
|
||||
|
||||
serverMajorVersion = _serverMajorVersion;
|
||||
|
||||
memcpy(&originalCallbacks, clCallbacks, sizeof(originalCallbacks));
|
||||
memcpy(&platformCallbacks, plCallbacks, sizeof(platformCallbacks));
|
||||
|
||||
|
@ -20,22 +20,59 @@ static PCONNECTION_LISTENER_CALLBACKS listenerCallbacks;
|
||||
static int lossCountSinceLastReport = 0;
|
||||
static long currentFrame = 0;
|
||||
|
||||
#define PTYPE_START_STREAM_A 0x0606
|
||||
#define PPAYLEN_START_STREAM_A 2
|
||||
static const char PPAYLOAD_START_STREAM_A[PPAYLEN_START_STREAM_A] = { 0, 0 };
|
||||
#define IDX_START_A 0
|
||||
#define IDX_START_B 1
|
||||
#define IDX_RESYNC 2
|
||||
#define IDX_LOSS_STATS 3
|
||||
|
||||
#define PTYPE_START_STREAM_B 0x0609
|
||||
#define PPAYLEN_START_STREAM_B 1
|
||||
static const char PPAYLOAD_START_STREAM_B[PPAYLEN_START_STREAM_B] = { 0 };
|
||||
static const short packetTypesGen3[] = {
|
||||
0x140b, // Start A
|
||||
0x1410, // Start B
|
||||
0x1404, // Resync
|
||||
0x140c, // Loss Stats
|
||||
0x1417, // Frame Stats (unused)
|
||||
};
|
||||
static const short packetTypesGen4[] = {
|
||||
0x0606, // Start A
|
||||
0x0609, // Start B
|
||||
0x0604, // Resync
|
||||
0x060a, // Loss Stats
|
||||
0x0611, // Frame Stats (unused)
|
||||
};
|
||||
|
||||
#define PTYPE_RESYNC 0x0604
|
||||
#define PPAYLEN_RESYNC 24
|
||||
static const char startAGen3[] = {0};
|
||||
static const int startBGen3[] = {0, 0, 0, 0xa};
|
||||
|
||||
#define PTYPE_LOSS_STATS 0x060a
|
||||
#define PPAYLEN_LOSS_STATS 32
|
||||
static const char startAGen4[] = {0, 0};
|
||||
static const char startBGen4[] = {0};
|
||||
|
||||
#define PTYPE_FRAME_STATS 0x0611
|
||||
#define PPAYLEN_FRAME_STATS 64
|
||||
static const short payloadLengthsGen3[] = {
|
||||
sizeof(startAGen3), // Start A
|
||||
sizeof(startBGen3), // Start B
|
||||
24, // Resync
|
||||
32, // Loss Stats
|
||||
64, // Frame Stats
|
||||
};
|
||||
static const short payloadLengthsGen4[] = {
|
||||
sizeof(startAGen4), // Start A
|
||||
sizeof(startBGen4), // Start B
|
||||
24, // Resync
|
||||
32, // Loss Stats
|
||||
64, // Frame Stats
|
||||
};
|
||||
|
||||
static const char* preconstructedPayloadsGen3[] = {
|
||||
startAGen3,
|
||||
(char*)startBGen3
|
||||
};
|
||||
static const char* preconstructedPayloadsGen4[] = {
|
||||
startAGen4,
|
||||
startBGen4
|
||||
};
|
||||
|
||||
static short *packetTypes;
|
||||
static short *payloadLengths;
|
||||
static char **preconstructedPayloads;
|
||||
|
||||
#define LOSS_REPORT_INTERVAL_MS 50
|
||||
|
||||
@ -48,6 +85,17 @@ int initializeControlStream(IP_ADDRESS addr, PSTREAM_CONFIGURATION streamConfigP
|
||||
host = addr;
|
||||
listenerCallbacks = clCallbacks;
|
||||
|
||||
if (serverMajorVersion == 3) {
|
||||
packetTypes = (short*)packetTypesGen3;
|
||||
payloadLengths = (short*)payloadLengthsGen3;
|
||||
preconstructedPayloads = (char**)preconstructedPayloadsGen3;
|
||||
}
|
||||
else {
|
||||
packetTypes = (short*)packetTypesGen4;
|
||||
payloadLengths = (short*)payloadLengthsGen4;
|
||||
preconstructedPayloads = (char**)preconstructedPayloadsGen4;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -156,12 +204,12 @@ static int sendMessageAndDiscardReply(short ptype, short paylen, const void* pay
|
||||
}
|
||||
|
||||
static void lossStatsThreadFunc(void* context) {
|
||||
char lossStatsPayload[PPAYLEN_LOSS_STATS];
|
||||
char lossStatsPayload[payloadLengths[IDX_LOSS_STATS]];
|
||||
BYTE_BUFFER byteBuffer;
|
||||
|
||||
while (!PltIsThreadInterrupted(&lossStatsThread)) {
|
||||
// Construct the payload
|
||||
BbInitializeWrappedBuffer(&byteBuffer, lossStatsPayload, 0, PPAYLEN_LOSS_STATS, BYTE_ORDER_LITTLE);
|
||||
BbInitializeWrappedBuffer(&byteBuffer, lossStatsPayload, 0, payloadLengths[IDX_LOSS_STATS], BYTE_ORDER_LITTLE);
|
||||
BbPutInt(&byteBuffer, lossCountSinceLastReport);
|
||||
BbPutInt(&byteBuffer, LOSS_REPORT_INTERVAL_MS);
|
||||
BbPutInt(&byteBuffer, 1000);
|
||||
@ -171,8 +219,8 @@ static void lossStatsThreadFunc(void* context) {
|
||||
BbPutInt(&byteBuffer, 0x14);
|
||||
|
||||
// Send the message (and don't expect a response)
|
||||
if (!sendMessageAndForget(PTYPE_LOSS_STATS,
|
||||
PPAYLEN_LOSS_STATS, lossStatsPayload)) {
|
||||
if (!sendMessageAndForget(packetTypes[IDX_LOSS_STATS],
|
||||
payloadLengths[IDX_LOSS_STATS], lossStatsPayload)) {
|
||||
Limelog("Loss stats thread terminating #1\n");
|
||||
listenerCallbacks->connectionTerminated(LastSocketError());
|
||||
return;
|
||||
@ -202,7 +250,7 @@ static void resyncThreadFunc(void* context) {
|
||||
PltClearEvent(&resyncEvent);
|
||||
|
||||
// Send the resync request and read the response
|
||||
if (!sendMessageAndDiscardReply(PTYPE_RESYNC, PPAYLEN_RESYNC, payload)) {
|
||||
if (!sendMessageAndDiscardReply(packetTypes[IDX_RESYNC], payloadLengths[IDX_RESYNC], payload)) {
|
||||
Limelog("Resync thread terminating #1\n");
|
||||
listenerCallbacks->connectionTerminated(LastSocketError());
|
||||
return;
|
||||
@ -242,16 +290,16 @@ int startControlStream(void) {
|
||||
enableNoDelay(ctlSock);
|
||||
|
||||
// Send START A
|
||||
if (!sendMessageAndDiscardReply(PTYPE_START_STREAM_A,
|
||||
PPAYLEN_START_STREAM_A,
|
||||
PPAYLOAD_START_STREAM_A)) {
|
||||
if (!sendMessageAndDiscardReply(packetTypes[IDX_START_A],
|
||||
payloadLengths[IDX_START_A],
|
||||
preconstructedPayloads[IDX_START_A])) {
|
||||
return LastSocketError();
|
||||
}
|
||||
|
||||
// Send START B
|
||||
if (!sendMessageAndDiscardReply(PTYPE_START_STREAM_B,
|
||||
PPAYLEN_START_STREAM_B,
|
||||
PPAYLOAD_START_STREAM_B)) {
|
||||
if (!sendMessageAndDiscardReply(packetTypes[IDX_START_B],
|
||||
payloadLengths[IDX_START_B],
|
||||
preconstructedPayloads[IDX_START_B])) {
|
||||
return LastSocketError();
|
||||
}
|
||||
|
||||
|
@ -6,11 +6,10 @@
|
||||
#include "PlatformThreads.h"
|
||||
#include "Video.h"
|
||||
|
||||
/* GFE 2.2.2+ RTSP/SDP version code */
|
||||
#define RTSP_CLIENT_VERSION 11
|
||||
#define RTSP_CLIENT_VERSION_S "11"
|
||||
extern int serverMajorVersion;
|
||||
|
||||
char* getSdpPayloadForStreamConfig(PSTREAM_CONFIGURATION streamConfig, struct in_addr targetAddress, int *length);
|
||||
char* getSdpPayloadForStreamConfig(PSTREAM_CONFIGURATION streamConfig, struct in_addr targetAddress,
|
||||
int rtspClientVersion, int *length);
|
||||
|
||||
int initializeControlStream(IP_ADDRESS host, PSTREAM_CONFIGURATION streamConfig, PCONNECTION_LISTENER_CALLBACKS clCallbacks);
|
||||
int startControlStream(void);
|
||||
|
@ -103,7 +103,7 @@ typedef struct _PLATFORM_CALLBACKS {
|
||||
|
||||
int LiStartConnection(IP_ADDRESS host, PSTREAM_CONFIGURATION streamConfig, PCONNECTION_LISTENER_CALLBACKS clCallbacks,
|
||||
PDECODER_RENDERER_CALLBACKS drCallbacks, PAUDIO_RENDERER_CALLBACKS arCallbacks, PPLATFORM_CALLBACKS plCallbacks,
|
||||
void* renderContext, int drFlags);
|
||||
void* renderContext, int drFlags, int _serverMajorVersion);
|
||||
void LiStopConnection(void);
|
||||
const char* LiGetStageName(int stage);
|
||||
|
||||
|
@ -10,6 +10,7 @@ static char rtspTargetUrl[256];
|
||||
static char sessionIdString[16];
|
||||
static int hasSessionId;
|
||||
static char responseBuffer[RTSP_MAX_RESP_SIZE];
|
||||
static int rtspClientVersion;
|
||||
|
||||
/* Create RTSP Option */
|
||||
static POPTION_ITEM createOptionItem(char* option, char* content)
|
||||
@ -60,14 +61,16 @@ static int addOption(PRTSP_MESSAGE msg, char* option, char* content)
|
||||
static int initializeRtspRequest(PRTSP_MESSAGE msg, char* command, char* target)
|
||||
{
|
||||
char sequenceNumberStr[16];
|
||||
char clientVersionStr[16];
|
||||
|
||||
// FIXME: Hacked CSeq attribute due to RTSP parser bug
|
||||
createRtspRequest(msg, NULL, 0, command, target, "RTSP/1.0",
|
||||
0, NULL, NULL, 0);
|
||||
|
||||
sprintf(sequenceNumberStr, "%d", currentSeqNumber++);
|
||||
sprintf(clientVersionStr, "%d", rtspClientVersion);
|
||||
if (!addOption(msg, "CSeq", sequenceNumberStr) ||
|
||||
!addOption(msg, "X-GS-ClientVersion", RTSP_CLIENT_VERSION_S)) {
|
||||
!addOption(msg, "X-GS-ClientVersion", clientVersionStr)) {
|
||||
freeMessage(msg);
|
||||
return 0;
|
||||
}
|
||||
@ -247,7 +250,8 @@ static int sendVideoAnnounce(PRTSP_MESSAGE response, PSTREAM_CONFIGURATION strea
|
||||
}
|
||||
|
||||
memcpy(&sdpAddr, &remoteAddr, sizeof(remoteAddr));
|
||||
request.payload = getSdpPayloadForStreamConfig(streamConfig, sdpAddr, &payloadLength);
|
||||
request.payload = getSdpPayloadForStreamConfig(streamConfig, sdpAddr,
|
||||
rtspClientVersion, &payloadLength);
|
||||
if (request.payload == NULL) {
|
||||
goto FreeMessage;
|
||||
}
|
||||
@ -279,6 +283,13 @@ int performRtspHandshake(IP_ADDRESS addr, PSTREAM_CONFIGURATION streamConfigPtr)
|
||||
currentSeqNumber = 1;
|
||||
hasSessionId = 0;
|
||||
|
||||
if (serverMajorVersion == 3) {
|
||||
rtspClientVersion = 10;
|
||||
}
|
||||
else {
|
||||
rtspClientVersion = 11;
|
||||
}
|
||||
|
||||
{
|
||||
RTSP_MESSAGE response;
|
||||
|
||||
|
@ -88,6 +88,62 @@ static int addAttributeString(PSDP_OPTION *head, char* name, const char* payload
|
||||
return addAttributeBinary(head, name, payload, (int)strlen(payload));
|
||||
}
|
||||
|
||||
static int addGen3Options(PSDP_OPTION *head, char* addrStr) {
|
||||
int payloadInt;
|
||||
int err = 0;
|
||||
|
||||
err |= addAttributeString(head, "x-nv-general.serverAddress", addrStr);
|
||||
|
||||
payloadInt = htonl(0x42774141);
|
||||
err |= addAttributeBinary(head,
|
||||
"x-nv-general.featureFlags", &payloadInt, sizeof(payloadInt));
|
||||
|
||||
|
||||
payloadInt = htonl(0x41514141);
|
||||
err |= addAttributeBinary(head,
|
||||
"x-nv-video[0].transferProtocol", &payloadInt, sizeof(payloadInt));
|
||||
err |= addAttributeBinary(head,
|
||||
"x-nv-video[1].transferProtocol", &payloadInt, sizeof(payloadInt));
|
||||
err |= addAttributeBinary(head,
|
||||
"x-nv-video[2].transferProtocol", &payloadInt, sizeof(payloadInt));
|
||||
err |= addAttributeBinary(head,
|
||||
"x-nv-video[3].transferProtocol", &payloadInt, sizeof(payloadInt));
|
||||
|
||||
payloadInt = htonl(0x42414141);
|
||||
err |= addAttributeBinary(head,
|
||||
"x-nv-video[0].rateControlMode", &payloadInt, sizeof(payloadInt));
|
||||
payloadInt = htonl(0x42514141);
|
||||
err |= addAttributeBinary(head,
|
||||
"x-nv-video[1].rateControlMode", &payloadInt, sizeof(payloadInt));
|
||||
err |= addAttributeBinary(head,
|
||||
"x-nv-video[2].rateControlMode", &payloadInt, sizeof(payloadInt));
|
||||
err |= addAttributeBinary(head,
|
||||
"x-nv-video[3].rateControlMode", &payloadInt, sizeof(payloadInt));
|
||||
|
||||
err |= addAttributeString(head, "x-nv-vqos[0].bw.flags", "14083");
|
||||
|
||||
err |= addAttributeString(head, "x-nv-vqos[0].videoQosMaxConsecutiveDrops", "0");
|
||||
err |= addAttributeString(head, "x-nv-vqos[1].videoQosMaxConsecutiveDrops", "0");
|
||||
err |= addAttributeString(head, "x-nv-vqos[2].videoQosMaxConsecutiveDrops", "0");
|
||||
err |= addAttributeString(head, "x-nv-vqos[3].videoQosMaxConsecutiveDrops", "0");
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
static int addGen4Options(PSDP_OPTION *head, char* addrStr) {
|
||||
char payloadStr[92];
|
||||
int err = 0;
|
||||
|
||||
sprintf(payloadStr, "rtsp://%s:48010", addrStr);
|
||||
err |= addAttributeString(head, "x-nv-general.serverAddress", payloadStr);
|
||||
|
||||
err |= addAttributeString(head, "x-nv-video[0].rateControlMode", "4");
|
||||
|
||||
err |= addAttributeString(head, "x-nv-vqos[0].bw.flags", "51");
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
static PSDP_OPTION getAttributesList(PSTREAM_CONFIGURATION streamConfig, struct in_addr targetAddress) {
|
||||
PSDP_OPTION optionHead;
|
||||
char payloadStr[92];
|
||||
@ -96,9 +152,6 @@ static PSDP_OPTION getAttributesList(PSTREAM_CONFIGURATION streamConfig, struct
|
||||
optionHead = NULL;
|
||||
err = 0;
|
||||
|
||||
sprintf(payloadStr, "rtsp://%s:48010", inet_ntoa(targetAddress));
|
||||
err |= addAttributeString(&optionHead, "x-nv-general.serverAddress", payloadStr);
|
||||
|
||||
sprintf(payloadStr, "%d", streamConfig->width);
|
||||
err |= addAttributeString(&optionHead, "x-nv-video[0].clientViewportWd", payloadStr);
|
||||
sprintf(payloadStr, "%d", streamConfig->height);
|
||||
@ -121,9 +174,6 @@ static PSDP_OPTION getAttributesList(PSTREAM_CONFIGURATION streamConfig, struct
|
||||
err |= addAttributeString(&optionHead, "x-nv-video[0].timeoutLengthMs", "7000");
|
||||
err |= addAttributeString(&optionHead, "x-nv-video[0].framesWithInvalidRefThreshold", "0");
|
||||
|
||||
// This flags value will mean that resolution won't change as bitrate falls
|
||||
err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.flags", "51");
|
||||
|
||||
// Lock the bitrate since we're not scaling resolution so the picture doesn't get too bad
|
||||
if (streamConfig->height >= 1080 && streamConfig->fps >= 60) {
|
||||
if (streamConfig->bitrate < 10000) {
|
||||
@ -168,6 +218,13 @@ static PSDP_OPTION getAttributesList(PSTREAM_CONFIGURATION streamConfig, struct
|
||||
err |= addAttributeString(&optionHead, "x-nv-vqos[0].qosTrafficType", "5");
|
||||
err |= addAttributeString(&optionHead, "x-nv-aqos.qosTrafficType", "4");
|
||||
|
||||
if (serverMajorVersion == 3) {
|
||||
err |= addGen3Options(&optionHead, inet_ntoa(targetAddress));
|
||||
}
|
||||
else {
|
||||
err |= addGen4Options(&optionHead, inet_ntoa(targetAddress));
|
||||
}
|
||||
|
||||
if (err == 0) {
|
||||
return optionHead;
|
||||
}
|
||||
@ -177,22 +234,24 @@ static PSDP_OPTION getAttributesList(PSTREAM_CONFIGURATION streamConfig, struct
|
||||
}
|
||||
|
||||
/* Populate the SDP header with required information */
|
||||
static int fillSdpHeader(char* buffer, struct in_addr targetAddress) {
|
||||
static int fillSdpHeader(char* buffer, struct in_addr targetAddress, int rtspClientVersion) {
|
||||
return sprintf(buffer,
|
||||
"v=0\r\n"
|
||||
"o=android 0 "RTSP_CLIENT_VERSION_S" IN IPv4 %s\r\n"
|
||||
"s=NVIDIA Streaming Client\r\n", inet_ntoa(targetAddress));
|
||||
"o=android 0 %d IN IPv4 %s\r\n"
|
||||
"s=NVIDIA Streaming Client\r\n", rtspClientVersion, inet_ntoa(targetAddress));
|
||||
}
|
||||
|
||||
/* Populate the SDP tail with required information */
|
||||
static int fillSdpTail(char* buffer) {
|
||||
return sprintf(buffer,
|
||||
"t=0 0\r\n"
|
||||
"m=video 47998 \r\n");
|
||||
"m=video %d \r\n",
|
||||
serverMajorVersion < 4 ? 47996 : 47998);
|
||||
}
|
||||
|
||||
/* Get the SDP attributes for the stream config */
|
||||
char* getSdpPayloadForStreamConfig(PSTREAM_CONFIGURATION streamConfig, struct in_addr targetAddress, int *length) {
|
||||
char* getSdpPayloadForStreamConfig(PSTREAM_CONFIGURATION streamConfig, struct in_addr targetAddress,
|
||||
int rtspClientVersion, int *length) {
|
||||
PSDP_OPTION attributeList;
|
||||
int offset;
|
||||
char* payload;
|
||||
@ -209,7 +268,7 @@ char* getSdpPayloadForStreamConfig(PSTREAM_CONFIGURATION streamConfig, struct in
|
||||
return NULL;
|
||||
}
|
||||
|
||||
offset = fillSdpHeader(payload, targetAddress);
|
||||
offset = fillSdpHeader(payload, targetAddress, rtspClientVersion);
|
||||
offset += fillSerializedAttributeList(&payload[offset], attributeList);
|
||||
offset += fillSdpTail(&payload[offset]);
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
#define FIRST_FRAME_MAX 1500
|
||||
|
||||
#define RTP_PORT 47998
|
||||
#define FIRST_FRAME_PORT 47996
|
||||
|
||||
static DECODER_RENDERER_CALLBACKS callbacks;
|
||||
static STREAM_CONFIGURATION configuration;
|
||||
@ -13,6 +14,7 @@ static IP_ADDRESS remoteHost;
|
||||
static PCONNECTION_LISTENER_CALLBACKS listenerCallbacks;
|
||||
|
||||
static SOCKET rtpSocket = INVALID_SOCKET;
|
||||
static SOCKET firstFrameSocket = INVALID_SOCKET;
|
||||
|
||||
static PLT_THREAD udpPingThread;
|
||||
static PLT_THREAD receiveThread;
|
||||
@ -108,6 +110,17 @@ static void DecoderThreadProc(void* context) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Read the first frame of the video stream */
|
||||
int readFirstFrame(void) {
|
||||
// All that matters is that we close this socket.
|
||||
// This starts the flow of video on Gen 3 servers.
|
||||
|
||||
closesocket(firstFrameSocket);
|
||||
firstFrameSocket = INVALID_SOCKET;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Terminate the video stream */
|
||||
void stopVideoStream(void) {
|
||||
callbacks.stop();
|
||||
@ -116,6 +129,10 @@ void stopVideoStream(void) {
|
||||
PltInterruptThread(&receiveThread);
|
||||
PltInterruptThread(&decoderThread);
|
||||
|
||||
if (firstFrameSocket != INVALID_SOCKET) {
|
||||
closesocket(firstFrameSocket);
|
||||
firstFrameSocket = INVALID_SOCKET;
|
||||
}
|
||||
if (rtpSocket != INVALID_SOCKET) {
|
||||
closesocket(rtpSocket);
|
||||
rtpSocket = INVALID_SOCKET;
|
||||
@ -156,6 +173,14 @@ int startVideoStream(void* rendererContext, int drFlags) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (serverMajorVersion == 3) {
|
||||
// Connect this socket to open port 47998 for our ping thread
|
||||
firstFrameSocket = connectTcpSocket(remoteHost, FIRST_FRAME_PORT);
|
||||
if (firstFrameSocket == INVALID_SOCKET) {
|
||||
return LastSocketError();
|
||||
}
|
||||
}
|
||||
|
||||
// Start pinging before reading the first frame so GFE knows where
|
||||
// to send UDP data
|
||||
err = PltCreateThread(UdpPingThreadProc, NULL, &udpPingThread);
|
||||
@ -163,5 +188,13 @@ int startVideoStream(void* rendererContext, int drFlags) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (serverMajorVersion == 3) {
|
||||
// Read the first frame to start the flow of video
|
||||
err = readFirstFrame();
|
||||
if (err != 0) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user