diff options
author | Andreas Huber <andih@google.com> | 2012-09-04 11:27:30 -0700 |
---|---|---|
committer | Android Git Automerger <android-git-automerger@android.com> | 2012-09-04 11:27:30 -0700 |
commit | 09359d4e7b9186eec9ce744257a8d1dcb6cbbf15 (patch) | |
tree | e2139bf7738f4cd4d942e9522599ecb5084e4eea | |
parent | 23ed3d9cd183c845bed846ecf050aa8cbd6f4c0f (diff) | |
parent | 04b1b910664c87b4bd593176ba59e43c5ddfd162 (diff) | |
download | frameworks_av-09359d4e7b9186eec9ce744257a8d1dcb6cbbf15.zip frameworks_av-09359d4e7b9186eec9ce744257a8d1dcb6cbbf15.tar.gz frameworks_av-09359d4e7b9186eec9ce744257a8d1dcb6cbbf15.tar.bz2 |
am eca95af8: am fbe9d81f: Support for acting as a wifi display sink.
* commit 'eca95af88949ec76533927f39118ac57f4f01911':
Support for acting as a wifi display sink.
-rw-r--r-- | media/libmediaplayerservice/nuplayer/NuPlayer.cpp | 4 | ||||
-rw-r--r-- | media/libstagefright/wifi-display/Android.mk | 4 | ||||
-rw-r--r-- | media/libstagefright/wifi-display/sink/LinearRegression.cpp | 110 | ||||
-rw-r--r-- | media/libstagefright/wifi-display/sink/LinearRegression.h | 52 | ||||
-rw-r--r-- | media/libstagefright/wifi-display/sink/RTPSink.cpp | 806 | ||||
-rw-r--r-- | media/libstagefright/wifi-display/sink/RTPSink.h | 98 | ||||
-rw-r--r-- | media/libstagefright/wifi-display/sink/TunnelRenderer.cpp | 396 | ||||
-rw-r--r-- | media/libstagefright/wifi-display/sink/TunnelRenderer.h | 84 | ||||
-rw-r--r-- | media/libstagefright/wifi-display/wfd.cpp | 11 |
9 files changed, 1553 insertions, 12 deletions
diff --git a/media/libmediaplayerservice/nuplayer/NuPlayer.cpp b/media/libmediaplayerservice/nuplayer/NuPlayer.cpp index 564a256..62381c1 100644 --- a/media/libmediaplayerservice/nuplayer/NuPlayer.cpp +++ b/media/libmediaplayerservice/nuplayer/NuPlayer.cpp @@ -293,8 +293,8 @@ void NuPlayer::onMessageReceived(const sp<AMessage> &msg) { break; } - if (mAudioDecoder == NULL && mAudioSink != NULL || - mVideoDecoder == NULL && mNativeWindow != NULL) { + if ((mAudioDecoder == NULL && mAudioSink != NULL) + || (mVideoDecoder == NULL && mNativeWindow != NULL)) { msg->post(100000ll); mScanSourcesPending = true; } diff --git a/media/libstagefright/wifi-display/Android.mk b/media/libstagefright/wifi-display/Android.mk index b035a51..0e59b9e 100644 --- a/media/libstagefright/wifi-display/Android.mk +++ b/media/libstagefright/wifi-display/Android.mk @@ -5,6 +5,10 @@ include $(CLEAR_VARS) LOCAL_SRC_FILES:= \ ANetworkSession.cpp \ ParsedMessage.cpp \ + sink/LinearRegression.cpp \ + sink/RTPSink.cpp \ + sink/TunnelRenderer.cpp \ + sink/WifiDisplaySink.cpp \ source/Converter.cpp \ source/PlaybackSession.cpp \ source/RepeaterSource.cpp \ diff --git a/media/libstagefright/wifi-display/sink/LinearRegression.cpp b/media/libstagefright/wifi-display/sink/LinearRegression.cpp new file mode 100644 index 0000000..8cfce37 --- /dev/null +++ b/media/libstagefright/wifi-display/sink/LinearRegression.cpp @@ -0,0 +1,110 @@ +/* + * Copyright 2012, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "LinearRegression" +#include <utils/Log.h> + +#include "LinearRegression.h" + +#include <math.h> +#include <string.h> + +namespace android { + +LinearRegression::LinearRegression(size_t historySize) + : mHistorySize(historySize), + mCount(0), + mHistory(new Point[mHistorySize]), + mSumX(0.0), + mSumY(0.0) { +} + +LinearRegression::~LinearRegression() { + delete[] mHistory; + mHistory = NULL; +} + +void LinearRegression::addPoint(float x, float y) { + if (mCount == mHistorySize) { + const Point &oldest = mHistory[0]; + + mSumX -= oldest.mX; + mSumY -= oldest.mY; + + memmove(&mHistory[0], &mHistory[1], (mHistorySize - 1) * sizeof(Point)); + --mCount; + } + + Point *newest = &mHistory[mCount++]; + newest->mX = x; + newest->mY = y; + + mSumX += x; + mSumY += y; +} + +bool LinearRegression::approxLine(float *n1, float *n2, float *b) const { + static const float kEpsilon = 1.0E-4; + + if (mCount < 2) { + return false; + } + + float sumX2 = 0.0f; + float sumY2 = 0.0f; + float sumXY = 0.0f; + + float meanX = mSumX / (float)mCount; + float meanY = mSumY / (float)mCount; + + for (size_t i = 0; i < mCount; ++i) { + const Point &p = mHistory[i]; + + float x = p.mX - meanX; + float y = p.mY - meanY; + + sumX2 += x * x; + sumY2 += y * y; + sumXY += x * y; + } + + float T = sumX2 + sumY2; + float D = sumX2 * sumY2 - sumXY * sumXY; + float root = sqrt(T * T * 0.25 - D); + + float L1 = T * 0.5 - root; + + if (fabs(sumXY) > kEpsilon) { + *n1 = 1.0; + *n2 = (2.0 * L1 - sumX2) / sumXY; + + float mag = sqrt((*n1) * (*n1) + (*n2) * (*n2)); + + *n1 /= mag; + *n2 /= mag; + } else { + *n1 = 0.0; + *n2 = 1.0; + } + + *b = (*n1) * meanX + (*n2) * meanY; + + return true; +} + +} // namespace android + diff --git a/media/libstagefright/wifi-display/sink/LinearRegression.h b/media/libstagefright/wifi-display/sink/LinearRegression.h new file mode 100644 index 0000000..ca6f5a1 --- /dev/null +++ b/media/libstagefright/wifi-display/sink/LinearRegression.h @@ -0,0 +1,52 @@ +/* + * Copyright 2012, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LINEAR_REGRESSION_H_ + +#define LINEAR_REGRESSION_H_ + +#include <sys/types.h> +#include <media/stagefright/foundation/ABase.h> + +namespace android { + +// Helper class to fit a line to a set of points minimizing the sum of +// squared (orthogonal) distances from line to individual points. +struct LinearRegression { + LinearRegression(size_t historySize); + ~LinearRegression(); + + void addPoint(float x, float y); + + bool approxLine(float *n1, float *n2, float *b) const; + +private: + struct Point { + float mX, mY; + }; + + size_t mHistorySize; + size_t mCount; + Point *mHistory; + + float mSumX, mSumY; + + DISALLOW_EVIL_CONSTRUCTORS(LinearRegression); +}; + +} // namespace android + +#endif // LINEAR_REGRESSION_H_ diff --git a/media/libstagefright/wifi-display/sink/RTPSink.cpp b/media/libstagefright/wifi-display/sink/RTPSink.cpp new file mode 100644 index 0000000..0918034 --- /dev/null +++ b/media/libstagefright/wifi-display/sink/RTPSink.cpp @@ -0,0 +1,806 @@ +/* + * Copyright 2012, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "RTPSink" +#include <utils/Log.h> + +#include "RTPSink.h" + +#include "ANetworkSession.h" +#include "TunnelRenderer.h" + +#include <media/stagefright/foundation/ABuffer.h> +#include <media/stagefright/foundation/ADebug.h> +#include <media/stagefright/foundation/AMessage.h> +#include <media/stagefright/foundation/hexdump.h> +#include <media/stagefright/MediaErrors.h> +#include <media/stagefright/Utils.h> + +namespace android { + +struct RTPSink::Source : public RefBase { + Source(uint16_t seq, const sp<ABuffer> &buffer, + const sp<AMessage> queueBufferMsg); + + bool updateSeq(uint16_t seq, const sp<ABuffer> &buffer); + + void addReportBlock(uint32_t ssrc, const sp<ABuffer> &buf); + +protected: + virtual ~Source(); + +private: + static const uint32_t kMinSequential = 2; + static const uint32_t kMaxDropout = 3000; + static const uint32_t kMaxMisorder = 100; + static const uint32_t kRTPSeqMod = 1u << 16; + + sp<AMessage> mQueueBufferMsg; + + uint16_t mMaxSeq; + uint32_t mCycles; + uint32_t mBaseSeq; + uint32_t mBadSeq; + uint32_t mProbation; + uint32_t mReceived; + uint32_t mExpectedPrior; + uint32_t mReceivedPrior; + + void initSeq(uint16_t seq); + void queuePacket(const sp<ABuffer> &buffer); + + DISALLOW_EVIL_CONSTRUCTORS(Source); +}; + +//////////////////////////////////////////////////////////////////////////////// + +RTPSink::Source::Source( + uint16_t seq, const sp<ABuffer> &buffer, + const sp<AMessage> queueBufferMsg) + : mQueueBufferMsg(queueBufferMsg), + mProbation(kMinSequential) { + initSeq(seq); + mMaxSeq = seq - 1; + + buffer->setInt32Data(mCycles | seq); + queuePacket(buffer); +} + +RTPSink::Source::~Source() { +} + +void RTPSink::Source::initSeq(uint16_t seq) { + mMaxSeq = seq; + mCycles = 0; + mBaseSeq = seq; + mBadSeq = kRTPSeqMod + 1; + mReceived = 0; + mExpectedPrior = 0; + mReceivedPrior = 0; +} + +bool RTPSink::Source::updateSeq(uint16_t seq, const sp<ABuffer> &buffer) { + uint16_t udelta = seq - mMaxSeq; + + if (mProbation) { + // Startup phase + + if (seq == mMaxSeq + 1) { + buffer->setInt32Data(mCycles | seq); + queuePacket(buffer); + + --mProbation; + mMaxSeq = seq; + if (mProbation == 0) { + initSeq(seq); + ++mReceived; + + return true; + } + } else { + // Packet out of sequence, restart startup phase + + mProbation = kMinSequential - 1; + mMaxSeq = seq; + +#if 0 + mPackets.clear(); + mTotalBytesQueued = 0; + ALOGI("XXX cleared packets"); +#endif + + buffer->setInt32Data(mCycles | seq); + queuePacket(buffer); + } + + return false; + } + + if (udelta < kMaxDropout) { + // In order, with permissible gap. + + if (seq < mMaxSeq) { + // Sequence number wrapped - count another 64K cycle + mCycles += kRTPSeqMod; + } + + mMaxSeq = seq; + } else if (udelta <= kRTPSeqMod - kMaxMisorder) { + // The sequence number made a very large jump + + if (seq == mBadSeq) { + // Two sequential packets -- assume that the other side + // restarted without telling us so just re-sync + // (i.e. pretend this was the first packet) + + initSeq(seq); + } else { + mBadSeq = (seq + 1) & (kRTPSeqMod - 1); + + return false; + } + } else { + // Duplicate or reordered packet. + } + + ++mReceived; + + buffer->setInt32Data(mCycles | seq); + queuePacket(buffer); + + return true; +} + +void RTPSink::Source::queuePacket(const sp<ABuffer> &buffer) { + sp<AMessage> msg = mQueueBufferMsg->dup(); + msg->setBuffer("buffer", buffer); + msg->post(); +} + +void RTPSink::Source::addReportBlock( + uint32_t ssrc, const sp<ABuffer> &buf) { + uint32_t extMaxSeq = mMaxSeq | mCycles; + uint32_t expected = extMaxSeq - mBaseSeq + 1; + + int64_t lost = (int64_t)expected - (int64_t)mReceived; + if (lost > 0x7fffff) { + lost = 0x7fffff; + } else if (lost < -0x800000) { + lost = -0x800000; + } + + uint32_t expectedInterval = expected - mExpectedPrior; + mExpectedPrior = expected; + + uint32_t receivedInterval = mReceived - mReceivedPrior; + mReceivedPrior = mReceived; + + int64_t lostInterval = expectedInterval - receivedInterval; + + uint8_t fractionLost; + if (expectedInterval == 0 || lostInterval <=0) { + fractionLost = 0; + } else { + fractionLost = (lostInterval << 8) / expectedInterval; + } + + uint8_t *ptr = buf->data() + buf->size(); + + ptr[0] = ssrc >> 24; + ptr[1] = (ssrc >> 16) & 0xff; + ptr[2] = (ssrc >> 8) & 0xff; + ptr[3] = ssrc & 0xff; + + ptr[4] = fractionLost; + + ptr[5] = (lost >> 16) & 0xff; + ptr[6] = (lost >> 8) & 0xff; + ptr[7] = lost & 0xff; + + ptr[8] = extMaxSeq >> 24; + ptr[9] = (extMaxSeq >> 16) & 0xff; + ptr[10] = (extMaxSeq >> 8) & 0xff; + ptr[11] = extMaxSeq & 0xff; + + // XXX TODO: + + ptr[12] = 0x00; // interarrival jitter + ptr[13] = 0x00; + ptr[14] = 0x00; + ptr[15] = 0x00; + + ptr[16] = 0x00; // last SR + ptr[17] = 0x00; + ptr[18] = 0x00; + ptr[19] = 0x00; + + ptr[20] = 0x00; // delay since last SR + ptr[21] = 0x00; + ptr[22] = 0x00; + ptr[23] = 0x00; +} + +//////////////////////////////////////////////////////////////////////////////// + +RTPSink::RTPSink( + const sp<ANetworkSession> &netSession, + const sp<ISurfaceTexture> &surfaceTex) + : mNetSession(netSession), + mSurfaceTex(surfaceTex), + mRTPPort(0), + mRTPSessionID(0), + mRTCPSessionID(0), + mFirstArrivalTimeUs(-1ll), + mNumPacketsReceived(0ll), + mRegression(1000), + mMaxDelayMs(-1ll) { +} + +RTPSink::~RTPSink() { + if (mRTCPSessionID != 0) { + mNetSession->destroySession(mRTCPSessionID); + } + + if (mRTPSessionID != 0) { + mNetSession->destroySession(mRTPSessionID); + } +} + +status_t RTPSink::init(bool useTCPInterleaving) { + if (useTCPInterleaving) { + return OK; + } + + int clientRtp; + + sp<AMessage> rtpNotify = new AMessage(kWhatRTPNotify, id()); + sp<AMessage> rtcpNotify = new AMessage(kWhatRTCPNotify, id()); + for (clientRtp = 15550;; clientRtp += 2) { + int32_t rtpSession; + status_t err = mNetSession->createUDPSession( + clientRtp, rtpNotify, &rtpSession); + + if (err != OK) { + ALOGI("failed to create RTP socket on port %d", clientRtp); + continue; + } + + int32_t rtcpSession; + err = mNetSession->createUDPSession( + clientRtp + 1, rtcpNotify, &rtcpSession); + + if (err == OK) { + mRTPPort = clientRtp; + mRTPSessionID = rtpSession; + mRTCPSessionID = rtcpSession; + break; + } + + ALOGI("failed to create RTCP socket on port %d", clientRtp + 1); + mNetSession->destroySession(rtpSession); + } + + if (mRTPPort == 0) { + return UNKNOWN_ERROR; + } + + return OK; +} + +int32_t RTPSink::getRTPPort() const { + return mRTPPort; +} + +void RTPSink::onMessageReceived(const sp<AMessage> &msg) { + switch (msg->what()) { + case kWhatRTPNotify: + case kWhatRTCPNotify: + { + int32_t reason; + CHECK(msg->findInt32("reason", &reason)); + + switch (reason) { + case ANetworkSession::kWhatError: + { + int32_t sessionID; + CHECK(msg->findInt32("sessionID", &sessionID)); + + int32_t err; + CHECK(msg->findInt32("err", &err)); + + AString detail; + CHECK(msg->findString("detail", &detail)); + + ALOGE("An error occurred in session %d (%d, '%s/%s').", + sessionID, + err, + detail.c_str(), + strerror(-err)); + + mNetSession->destroySession(sessionID); + + if (sessionID == mRTPSessionID) { + mRTPSessionID = 0; + } else if (sessionID == mRTCPSessionID) { + mRTCPSessionID = 0; + } + break; + } + + case ANetworkSession::kWhatDatagram: + { + int32_t sessionID; + CHECK(msg->findInt32("sessionID", &sessionID)); + + sp<ABuffer> data; + CHECK(msg->findBuffer("data", &data)); + + status_t err; + if (msg->what() == kWhatRTPNotify) { + err = parseRTP(data); + } else { + err = parseRTCP(data); + } + break; + } + + default: + TRESPASS(); + } + break; + } + + case kWhatSendRR: + { + onSendRR(); + break; + } + + case kWhatPacketLost: + { + onPacketLost(msg); + break; + } + + case kWhatInject: + { + int32_t isRTP; + CHECK(msg->findInt32("isRTP", &isRTP)); + + sp<ABuffer> buffer; + CHECK(msg->findBuffer("buffer", &buffer)); + + status_t err; + if (isRTP) { + err = parseRTP(buffer); + } else { + err = parseRTCP(buffer); + } + break; + } + + default: + TRESPASS(); + } +} + +status_t RTPSink::injectPacket(bool isRTP, const sp<ABuffer> &buffer) { + sp<AMessage> msg = new AMessage(kWhatInject, id()); + msg->setInt32("isRTP", isRTP); + msg->setBuffer("buffer", buffer); + msg->post(); + + return OK; +} + +status_t RTPSink::parseRTP(const sp<ABuffer> &buffer) { + size_t size = buffer->size(); + if (size < 12) { + // Too short to be a valid RTP header. + return ERROR_MALFORMED; + } + + const uint8_t *data = buffer->data(); + + if ((data[0] >> 6) != 2) { + // Unsupported version. + return ERROR_UNSUPPORTED; + } + + if (data[0] & 0x20) { + // Padding present. + + size_t paddingLength = data[size - 1]; + + if (paddingLength + 12 > size) { + // If we removed this much padding we'd end up with something + // that's too short to be a valid RTP header. + return ERROR_MALFORMED; + } + + size -= paddingLength; + } + + int numCSRCs = data[0] & 0x0f; + + size_t payloadOffset = 12 + 4 * numCSRCs; + + if (size < payloadOffset) { + // Not enough data to fit the basic header and all the CSRC entries. + return ERROR_MALFORMED; + } + + if (data[0] & 0x10) { + // Header eXtension present. + + if (size < payloadOffset + 4) { + // Not enough data to fit the basic header, all CSRC entries + // and the first 4 bytes of the extension header. + + return ERROR_MALFORMED; + } + + const uint8_t *extensionData = &data[payloadOffset]; + + size_t extensionLength = + 4 * (extensionData[2] << 8 | extensionData[3]); + + if (size < payloadOffset + 4 + extensionLength) { + return ERROR_MALFORMED; + } + + payloadOffset += 4 + extensionLength; + } + + uint32_t srcId = U32_AT(&data[8]); + uint32_t rtpTime = U32_AT(&data[4]); + uint16_t seqNo = U16_AT(&data[2]); + + int64_t arrivalTimeUs; + CHECK(buffer->meta()->findInt64("arrivalTimeUs", &arrivalTimeUs)); + + if (mFirstArrivalTimeUs < 0ll) { + mFirstArrivalTimeUs = arrivalTimeUs; + } + arrivalTimeUs -= mFirstArrivalTimeUs; + + int64_t arrivalTimeMedia = (arrivalTimeUs * 9ll) / 100ll; + + ALOGV("seqNo: %d, SSRC 0x%08x, diff %lld", + seqNo, srcId, rtpTime - arrivalTimeMedia); + + mRegression.addPoint((float)rtpTime, (float)arrivalTimeMedia); + + ++mNumPacketsReceived; + + float n1, n2, b; + if (mRegression.approxLine(&n1, &n2, &b)) { + ALOGV("Line %lld: %.2f %.2f %.2f, slope %.2f", + mNumPacketsReceived, n1, n2, b, -n1 / n2); + + float expectedArrivalTimeMedia = (b - n1 * (float)rtpTime) / n2; + float latenessMs = (arrivalTimeMedia - expectedArrivalTimeMedia) / 90.0; + + if (mMaxDelayMs < 0ll || latenessMs > mMaxDelayMs) { + mMaxDelayMs = latenessMs; + ALOGI("packet was %.2f ms late", latenessMs); + } + } + + sp<AMessage> meta = buffer->meta(); + meta->setInt32("ssrc", srcId); + meta->setInt32("rtp-time", rtpTime); + meta->setInt32("PT", data[1] & 0x7f); + meta->setInt32("M", data[1] >> 7); + + buffer->setRange(payloadOffset, size - payloadOffset); + + ssize_t index = mSources.indexOfKey(srcId); + if (index < 0) { + if (mRenderer == NULL) { + sp<AMessage> notifyLost = new AMessage(kWhatPacketLost, id()); + notifyLost->setInt32("ssrc", srcId); + + mRenderer = new TunnelRenderer(notifyLost, mSurfaceTex); + looper()->registerHandler(mRenderer); + } + + sp<AMessage> queueBufferMsg = + new AMessage(TunnelRenderer::kWhatQueueBuffer, mRenderer->id()); + + sp<Source> source = new Source(seqNo, buffer, queueBufferMsg); + mSources.add(srcId, source); + } else { + mSources.valueAt(index)->updateSeq(seqNo, buffer); + } + + return OK; +} + +status_t RTPSink::parseRTCP(const sp<ABuffer> &buffer) { + const uint8_t *data = buffer->data(); + size_t size = buffer->size(); + + while (size > 0) { + if (size < 8) { + // Too short to be a valid RTCP header + return ERROR_MALFORMED; + } + + if ((data[0] >> 6) != 2) { + // Unsupported version. + return ERROR_UNSUPPORTED; + } + + if (data[0] & 0x20) { + // Padding present. + + size_t paddingLength = data[size - 1]; + + if (paddingLength + 12 > size) { + // If we removed this much padding we'd end up with something + // that's too short to be a valid RTP header. + return ERROR_MALFORMED; + } + + size -= paddingLength; + } + + size_t headerLength = 4 * (data[2] << 8 | data[3]) + 4; + + if (size < headerLength) { + // Only received a partial packet? + return ERROR_MALFORMED; + } + + switch (data[1]) { + case 200: + { + parseSR(data, headerLength); + break; + } + + case 201: // RR + case 202: // SDES + case 204: // APP + break; + + case 205: // TSFB (transport layer specific feedback) + case 206: // PSFB (payload specific feedback) + // hexdump(data, headerLength); + break; + + case 203: + { + parseBYE(data, headerLength); + break; + } + + default: + { + ALOGW("Unknown RTCP packet type %u of size %d", + (unsigned)data[1], headerLength); + break; + } + } + + data += headerLength; + size -= headerLength; + } + + return OK; +} + +status_t RTPSink::parseBYE(const uint8_t *data, size_t size) { + size_t SC = data[0] & 0x3f; + + if (SC == 0 || size < (4 + SC * 4)) { + // Packet too short for the minimal BYE header. + return ERROR_MALFORMED; + } + + uint32_t id = U32_AT(&data[4]); + + return OK; +} + +status_t RTPSink::parseSR(const uint8_t *data, size_t size) { + size_t RC = data[0] & 0x1f; + + if (size < (7 + RC * 6) * 4) { + // Packet too short for the minimal SR header. + return ERROR_MALFORMED; + } + + uint32_t id = U32_AT(&data[4]); + uint64_t ntpTime = U64_AT(&data[8]); + uint32_t rtpTime = U32_AT(&data[16]); + + ALOGV("SR: ssrc 0x%08x, ntpTime 0x%016llx, rtpTime 0x%08x", + id, ntpTime, rtpTime); + + return OK; +} + +status_t RTPSink::connect( + const char *host, int32_t remoteRtpPort, int32_t remoteRtcpPort) { + ALOGI("connecting RTP/RTCP sockets to %s:{%d,%d}", + host, remoteRtpPort, remoteRtcpPort); + + status_t err = + mNetSession->connectUDPSession(mRTPSessionID, host, remoteRtpPort); + + if (err != OK) { + return err; + } + + err = mNetSession->connectUDPSession(mRTCPSessionID, host, remoteRtcpPort); + + if (err != OK) { + return err; + } + +#if 0 + sp<ABuffer> buf = new ABuffer(1500); + memset(buf->data(), 0, buf->size()); + + mNetSession->sendRequest( + mRTPSessionID, buf->data(), buf->size()); + + mNetSession->sendRequest( + mRTCPSessionID, buf->data(), buf->size()); +#endif + + scheduleSendRR(); + + return OK; +} + +void RTPSink::scheduleSendRR() { + (new AMessage(kWhatSendRR, id()))->post(2000000ll); +} + +void RTPSink::addSDES(const sp<ABuffer> &buffer) { + uint8_t *data = buffer->data() + buffer->size(); + data[0] = 0x80 | 1; + data[1] = 202; // SDES + data[4] = 0xde; // SSRC + data[5] = 0xad; + data[6] = 0xbe; + data[7] = 0xef; + + size_t offset = 8; + + data[offset++] = 1; // CNAME + + AString cname = "stagefright@somewhere"; + data[offset++] = cname.size(); + + memcpy(&data[offset], cname.c_str(), cname.size()); + offset += cname.size(); + + data[offset++] = 6; // TOOL + + AString tool = "stagefright/1.0"; + data[offset++] = tool.size(); + + memcpy(&data[offset], tool.c_str(), tool.size()); + offset += tool.size(); + + data[offset++] = 0; + + if ((offset % 4) > 0) { + size_t count = 4 - (offset % 4); + switch (count) { + case 3: + data[offset++] = 0; + case 2: + data[offset++] = 0; + case 1: + data[offset++] = 0; + } + } + + size_t numWords = (offset / 4) - 1; + data[2] = numWords >> 8; + data[3] = numWords & 0xff; + + buffer->setRange(buffer->offset(), buffer->size() + offset); +} + +void RTPSink::onSendRR() { + sp<ABuffer> buf = new ABuffer(1500); + buf->setRange(0, 0); + + uint8_t *ptr = buf->data(); + ptr[0] = 0x80 | 0; + ptr[1] = 201; // RR + ptr[2] = 0; + ptr[3] = 1; + ptr[4] = 0xde; // SSRC + ptr[5] = 0xad; + ptr[6] = 0xbe; + ptr[7] = 0xef; + + buf->setRange(0, 8); + + size_t numReportBlocks = 0; + for (size_t i = 0; i < mSources.size(); ++i) { + uint32_t ssrc = mSources.keyAt(i); + sp<Source> source = mSources.valueAt(i); + + if (numReportBlocks > 31 || buf->size() + 24 > buf->capacity()) { + // Cannot fit another report block. + break; + } + + source->addReportBlock(ssrc, buf); + ++numReportBlocks; + } + + ptr[0] |= numReportBlocks; // 5 bit + + size_t sizeInWordsMinus1 = 1 + 6 * numReportBlocks; + ptr[2] = sizeInWordsMinus1 >> 8; + ptr[3] = sizeInWordsMinus1 & 0xff; + + buf->setRange(0, (sizeInWordsMinus1 + 1) * 4); + + addSDES(buf); + + mNetSession->sendRequest(mRTCPSessionID, buf->data(), buf->size()); + + scheduleSendRR(); +} + +void RTPSink::onPacketLost(const sp<AMessage> &msg) { + uint32_t srcId; + CHECK(msg->findInt32("ssrc", (int32_t *)&srcId)); + + int32_t seqNo; + CHECK(msg->findInt32("seqNo", &seqNo)); + + int32_t blp = 0; + + sp<ABuffer> buf = new ABuffer(1500); + buf->setRange(0, 0); + + uint8_t *ptr = buf->data(); + ptr[0] = 0x80 | 1; // generic NACK + ptr[1] = 205; // RTPFB + ptr[2] = 0; + ptr[3] = 3; + ptr[4] = 0xde; // sender SSRC + ptr[5] = 0xad; + ptr[6] = 0xbe; + ptr[7] = 0xef; + ptr[8] = (srcId >> 24) & 0xff; + ptr[9] = (srcId >> 16) & 0xff; + ptr[10] = (srcId >> 8) & 0xff; + ptr[11] = (srcId & 0xff); + ptr[12] = (seqNo >> 8) & 0xff; + ptr[13] = (seqNo & 0xff); + ptr[14] = (blp >> 8) & 0xff; + ptr[15] = (blp & 0xff); + + buf->setRange(0, 16); + + mNetSession->sendRequest(mRTCPSessionID, buf->data(), buf->size()); +} + +} // namespace android + diff --git a/media/libstagefright/wifi-display/sink/RTPSink.h b/media/libstagefright/wifi-display/sink/RTPSink.h new file mode 100644 index 0000000..a1d127d --- /dev/null +++ b/media/libstagefright/wifi-display/sink/RTPSink.h @@ -0,0 +1,98 @@ +/* + * Copyright 2012, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RTP_SINK_H_ + +#define RTP_SINK_H_ + +#include <media/stagefright/foundation/AHandler.h> + +#include "LinearRegression.h" + +#include <gui/Surface.h> + +namespace android { + +struct ABuffer; +struct ANetworkSession; +struct TunnelRenderer; + +// Creates a pair of sockets for RTP/RTCP traffic, instantiates a renderer +// for incoming transport stream data and occasionally sends statistics over +// the RTCP channel. +struct RTPSink : public AHandler { + RTPSink(const sp<ANetworkSession> &netSession, + const sp<ISurfaceTexture> &surfaceTex); + + // If TCP interleaving is used, no UDP sockets are created, instead + // incoming RTP/RTCP packets (arriving on the RTSP control connection) + // are manually injected by WifiDisplaySink. + status_t init(bool useTCPInterleaving); + + status_t connect( + const char *host, int32_t remoteRtpPort, int32_t remoteRtcpPort); + + int32_t getRTPPort() const; + + status_t injectPacket(bool isRTP, const sp<ABuffer> &buffer); + +protected: + virtual void onMessageReceived(const sp<AMessage> &msg); + virtual ~RTPSink(); + +private: + enum { + kWhatRTPNotify, + kWhatRTCPNotify, + kWhatSendRR, + kWhatPacketLost, + kWhatInject, + }; + + struct Source; + struct StreamSource; + + sp<ANetworkSession> mNetSession; + sp<ISurfaceTexture> mSurfaceTex; + KeyedVector<uint32_t, sp<Source> > mSources; + + int32_t mRTPPort; + int32_t mRTPSessionID; + int32_t mRTCPSessionID; + + int64_t mFirstArrivalTimeUs; + int64_t mNumPacketsReceived; + LinearRegression mRegression; + int64_t mMaxDelayMs; + + sp<TunnelRenderer> mRenderer; + + status_t parseRTP(const sp<ABuffer> &buffer); + status_t parseRTCP(const sp<ABuffer> &buffer); + status_t parseBYE(const uint8_t *data, size_t size); + status_t parseSR(const uint8_t *data, size_t size); + + void addSDES(const sp<ABuffer> &buffer); + void onSendRR(); + void onPacketLost(const sp<AMessage> &msg); + void scheduleSendRR(); + + DISALLOW_EVIL_CONSTRUCTORS(RTPSink); +}; + +} // namespace android + +#endif // RTP_SINK_H_ diff --git a/media/libstagefright/wifi-display/sink/TunnelRenderer.cpp b/media/libstagefright/wifi-display/sink/TunnelRenderer.cpp new file mode 100644 index 0000000..bc35aef --- /dev/null +++ b/media/libstagefright/wifi-display/sink/TunnelRenderer.cpp @@ -0,0 +1,396 @@ +/* + * Copyright 2012, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "TunnelRenderer" +#include <utils/Log.h> + +#include "TunnelRenderer.h" + +#include "ATSParser.h" + +#include <binder/IMemory.h> +#include <binder/IServiceManager.h> +#include <gui/SurfaceComposerClient.h> +#include <media/IMediaPlayerService.h> +#include <media/IStreamSource.h> +#include <media/stagefright/foundation/ABuffer.h> +#include <media/stagefright/foundation/ADebug.h> +#include <media/stagefright/foundation/AMessage.h> +#include <ui/DisplayInfo.h> + +namespace android { + +struct TunnelRenderer::PlayerClient : public BnMediaPlayerClient { + PlayerClient() {} + + virtual void notify(int msg, int ext1, int ext2, const Parcel *obj) { + ALOGI("notify %d, %d, %d", msg, ext1, ext2); + } + +protected: + virtual ~PlayerClient() {} + +private: + DISALLOW_EVIL_CONSTRUCTORS(PlayerClient); +}; + +struct TunnelRenderer::StreamSource : public BnStreamSource { + StreamSource(TunnelRenderer *owner); + + virtual void setListener(const sp<IStreamListener> &listener); + virtual void setBuffers(const Vector<sp<IMemory> > &buffers); + + virtual void onBufferAvailable(size_t index); + + virtual uint32_t flags() const; + + void doSomeWork(); + +protected: + virtual ~StreamSource(); + +private: + mutable Mutex mLock; + + TunnelRenderer *mOwner; + + sp<IStreamListener> mListener; + + Vector<sp<IMemory> > mBuffers; + List<size_t> mIndicesAvailable; + + size_t mNumDeqeued; + + DISALLOW_EVIL_CONSTRUCTORS(StreamSource); +}; + +//////////////////////////////////////////////////////////////////////////////// + +TunnelRenderer::StreamSource::StreamSource(TunnelRenderer *owner) + : mOwner(owner), + mNumDeqeued(0) { +} + +TunnelRenderer::StreamSource::~StreamSource() { +} + +void TunnelRenderer::StreamSource::setListener( + const sp<IStreamListener> &listener) { + mListener = listener; +} + +void TunnelRenderer::StreamSource::setBuffers( + const Vector<sp<IMemory> > &buffers) { + mBuffers = buffers; +} + +void TunnelRenderer::StreamSource::onBufferAvailable(size_t index) { + CHECK_LT(index, mBuffers.size()); + + { + Mutex::Autolock autoLock(mLock); + mIndicesAvailable.push_back(index); + } + + doSomeWork(); +} + +uint32_t TunnelRenderer::StreamSource::flags() const { + return kFlagAlignedVideoData; +} + +void TunnelRenderer::StreamSource::doSomeWork() { + Mutex::Autolock autoLock(mLock); + + while (!mIndicesAvailable.empty()) { + sp<ABuffer> srcBuffer = mOwner->dequeueBuffer(); + if (srcBuffer == NULL) { + break; + } + + ++mNumDeqeued; + + if (mNumDeqeued == 1) { + ALOGI("fixing real time now."); + + sp<AMessage> extra = new AMessage; + + extra->setInt32( + IStreamListener::kKeyDiscontinuityMask, + ATSParser::DISCONTINUITY_ABSOLUTE_TIME); + + extra->setInt64("timeUs", ALooper::GetNowUs()); + + mListener->issueCommand( + IStreamListener::DISCONTINUITY, + false /* synchronous */, + extra); + } + + ALOGV("dequeue TS packet of size %d", srcBuffer->size()); + + size_t index = *mIndicesAvailable.begin(); + mIndicesAvailable.erase(mIndicesAvailable.begin()); + + sp<IMemory> mem = mBuffers.itemAt(index); + CHECK_LE(srcBuffer->size(), mem->size()); + CHECK_EQ((srcBuffer->size() % 188), 0u); + + memcpy(mem->pointer(), srcBuffer->data(), srcBuffer->size()); + mListener->queueBuffer(index, srcBuffer->size()); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +TunnelRenderer::TunnelRenderer( + const sp<AMessage> ¬ifyLost, + const sp<ISurfaceTexture> &surfaceTex) + : mNotifyLost(notifyLost), + mSurfaceTex(surfaceTex), + mTotalBytesQueued(0ll), + mLastDequeuedExtSeqNo(-1), + mFirstFailedAttemptUs(-1ll), + mRequestedRetransmission(false) { +} + +TunnelRenderer::~TunnelRenderer() { + destroyPlayer(); +} + +void TunnelRenderer::queueBuffer(const sp<ABuffer> &buffer) { + Mutex::Autolock autoLock(mLock); + + mTotalBytesQueued += buffer->size(); + + if (mPackets.empty()) { + mPackets.push_back(buffer); + return; + } + + int32_t newExtendedSeqNo = buffer->int32Data(); + + List<sp<ABuffer> >::iterator firstIt = mPackets.begin(); + List<sp<ABuffer> >::iterator it = --mPackets.end(); + for (;;) { + int32_t extendedSeqNo = (*it)->int32Data(); + + if (extendedSeqNo == newExtendedSeqNo) { + // Duplicate packet. + return; + } + + if (extendedSeqNo < newExtendedSeqNo) { + // Insert new packet after the one at "it". + mPackets.insert(++it, buffer); + return; + } + + if (it == firstIt) { + // Insert new packet before the first existing one. + mPackets.insert(it, buffer); + return; + } + + --it; + } +} + +sp<ABuffer> TunnelRenderer::dequeueBuffer() { + Mutex::Autolock autoLock(mLock); + + sp<ABuffer> buffer; + int32_t extSeqNo; + while (!mPackets.empty()) { + buffer = *mPackets.begin(); + extSeqNo = buffer->int32Data(); + + if (mLastDequeuedExtSeqNo < 0 || extSeqNo > mLastDequeuedExtSeqNo) { + break; + } + + // This is a retransmission of a packet we've already returned. + + mTotalBytesQueued -= buffer->size(); + buffer.clear(); + extSeqNo = -1; + + mPackets.erase(mPackets.begin()); + } + + if (mPackets.empty()) { + if (mFirstFailedAttemptUs < 0ll) { + mFirstFailedAttemptUs = ALooper::GetNowUs(); + mRequestedRetransmission = false; + } else { + ALOGV("no packets available for %.2f secs", + (ALooper::GetNowUs() - mFirstFailedAttemptUs) / 1E6); + } + + return NULL; + } + + if (mLastDequeuedExtSeqNo < 0 || extSeqNo == mLastDequeuedExtSeqNo + 1) { + if (mRequestedRetransmission) { + ALOGI("Recovered after requesting retransmission of %d", + extSeqNo); + } + + mLastDequeuedExtSeqNo = extSeqNo; + mFirstFailedAttemptUs = -1ll; + mRequestedRetransmission = false; + + mPackets.erase(mPackets.begin()); + + mTotalBytesQueued -= buffer->size(); + + return buffer; + } + + if (mFirstFailedAttemptUs < 0ll) { + mFirstFailedAttemptUs = ALooper::GetNowUs(); + + ALOGI("failed to get the correct packet the first time."); + return NULL; + } + + if (mFirstFailedAttemptUs + 50000ll > ALooper::GetNowUs()) { + // We're willing to wait a little while to get the right packet. + + if (!mRequestedRetransmission) { + ALOGI("requesting retransmission of seqNo %d", + (mLastDequeuedExtSeqNo + 1) & 0xffff); + + sp<AMessage> notify = mNotifyLost->dup(); + notify->setInt32("seqNo", (mLastDequeuedExtSeqNo + 1) & 0xffff); + notify->post(); + + mRequestedRetransmission = true; + } else { + ALOGI("still waiting for the correct packet to arrive."); + } + + return NULL; + } + + ALOGI("dropping packet. extSeqNo %d didn't arrive in time", + mLastDequeuedExtSeqNo + 1); + + // Permanent failure, we never received the packet. + mLastDequeuedExtSeqNo = extSeqNo; + mFirstFailedAttemptUs = -1ll; + mRequestedRetransmission = false; + + mTotalBytesQueued -= buffer->size(); + + mPackets.erase(mPackets.begin()); + + return buffer; +} + +void TunnelRenderer::onMessageReceived(const sp<AMessage> &msg) { + switch (msg->what()) { + case kWhatQueueBuffer: + { + sp<ABuffer> buffer; + CHECK(msg->findBuffer("buffer", &buffer)); + + queueBuffer(buffer); + + if (mStreamSource == NULL) { + if (mTotalBytesQueued > 0ll) { + initPlayer(); + } else { + ALOGI("Have %lld bytes queued...", mTotalBytesQueued); + } + } else { + mStreamSource->doSomeWork(); + } + break; + } + + default: + TRESPASS(); + } +} + +void TunnelRenderer::initPlayer() { + if (mSurfaceTex == NULL) { + mComposerClient = new SurfaceComposerClient; + CHECK_EQ(mComposerClient->initCheck(), (status_t)OK); + + DisplayInfo info; + SurfaceComposerClient::getDisplayInfo(0, &info); + ssize_t displayWidth = info.w; + ssize_t displayHeight = info.h; + + mSurfaceControl = + mComposerClient->createSurface( + String8("A Surface"), + displayWidth, + displayHeight, + PIXEL_FORMAT_RGB_565, + 0); + + CHECK(mSurfaceControl != NULL); + CHECK(mSurfaceControl->isValid()); + + SurfaceComposerClient::openGlobalTransaction(); + CHECK_EQ(mSurfaceControl->setLayer(INT_MAX), (status_t)OK); + CHECK_EQ(mSurfaceControl->show(), (status_t)OK); + SurfaceComposerClient::closeGlobalTransaction(); + + mSurface = mSurfaceControl->getSurface(); + CHECK(mSurface != NULL); + } + + sp<IServiceManager> sm = defaultServiceManager(); + sp<IBinder> binder = sm->getService(String16("media.player")); + sp<IMediaPlayerService> service = interface_cast<IMediaPlayerService>(binder); + CHECK(service.get() != NULL); + + mStreamSource = new StreamSource(this); + + mPlayerClient = new PlayerClient; + + mPlayer = service->create(getpid(), mPlayerClient, 0); + CHECK(mPlayer != NULL); + CHECK_EQ(mPlayer->setDataSource(mStreamSource), (status_t)OK); + + mPlayer->setVideoSurfaceTexture( + mSurfaceTex != NULL ? mSurfaceTex : mSurface->getSurfaceTexture()); + + mPlayer->start(); +} + +void TunnelRenderer::destroyPlayer() { + mStreamSource.clear(); + + mPlayer->stop(); + mPlayer.clear(); + + if (mSurfaceTex == NULL) { + mSurface.clear(); + mSurfaceControl.clear(); + + mComposerClient->dispose(); + mComposerClient.clear(); + } +} + +} // namespace android + diff --git a/media/libstagefright/wifi-display/sink/TunnelRenderer.h b/media/libstagefright/wifi-display/sink/TunnelRenderer.h new file mode 100644 index 0000000..c9597e0 --- /dev/null +++ b/media/libstagefright/wifi-display/sink/TunnelRenderer.h @@ -0,0 +1,84 @@ +/* + * Copyright 2012, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef TUNNEL_RENDERER_H_ + +#define TUNNEL_RENDERER_H_ + +#include <gui/Surface.h> +#include <media/stagefright/foundation/AHandler.h> + +namespace android { + +struct ABuffer; +struct SurfaceComposerClient; +struct SurfaceControl; +struct Surface; +struct IMediaPlayer; +struct IStreamListener; + +// This class reassembles incoming RTP packets into the correct order +// and sends the resulting transport stream to a mediaplayer instance +// for playback. +struct TunnelRenderer : public AHandler { + TunnelRenderer( + const sp<AMessage> ¬ifyLost, + const sp<ISurfaceTexture> &surfaceTex); + + sp<ABuffer> dequeueBuffer(); + + enum { + kWhatQueueBuffer, + }; + +protected: + virtual void onMessageReceived(const sp<AMessage> &msg); + virtual ~TunnelRenderer(); + +private: + struct PlayerClient; + struct StreamSource; + + mutable Mutex mLock; + + sp<AMessage> mNotifyLost; + sp<ISurfaceTexture> mSurfaceTex; + + List<sp<ABuffer> > mPackets; + int64_t mTotalBytesQueued; + + sp<SurfaceComposerClient> mComposerClient; + sp<SurfaceControl> mSurfaceControl; + sp<Surface> mSurface; + sp<PlayerClient> mPlayerClient; + sp<IMediaPlayer> mPlayer; + sp<StreamSource> mStreamSource; + + int32_t mLastDequeuedExtSeqNo; + int64_t mFirstFailedAttemptUs; + bool mRequestedRetransmission; + + void initPlayer(); + void destroyPlayer(); + + void queueBuffer(const sp<ABuffer> &buffer); + + DISALLOW_EVIL_CONSTRUCTORS(TunnelRenderer); +}; + +} // namespace android + +#endif // TUNNEL_RENDERER_H_ diff --git a/media/libstagefright/wifi-display/wfd.cpp b/media/libstagefright/wifi-display/wfd.cpp index 5e7d9fd..d886f14 100644 --- a/media/libstagefright/wifi-display/wfd.cpp +++ b/media/libstagefright/wifi-display/wfd.cpp @@ -18,11 +18,8 @@ #define LOG_TAG "wfd" #include <utils/Log.h> -#define SUPPORT_SINK 0 - -#if SUPPORT_SINK #include "sink/WifiDisplaySink.h" -#endif +#include "source/WifiDisplaySource.h" #include <binder/ProcessState.h> #include <binder/IServiceManager.h> @@ -49,10 +46,8 @@ static void enableDisableRemoteDisplay(const char *iface) { static void usage(const char *me) { fprintf(stderr, "usage:\n" -#if SUPPORT_SINK " %s -c host[:port]\tconnect to wifi source\n" " -u uri \tconnect to an rtsp uri\n" -#endif " -e ip[:port] \tenable remote display\n" " -d \tdisable remote display\n", me); @@ -72,7 +67,6 @@ int main(int argc, char **argv) { int res; while ((res = getopt(argc, argv, "hc:l:u:e:d")) >= 0) { switch (res) { -#if SUPPORT_SINK case 'c': { const char *colonPos = strrchr(optarg, ':'); @@ -100,7 +94,6 @@ int main(int argc, char **argv) { uri = optarg; break; } -#endif case 'e': { @@ -124,7 +117,6 @@ int main(int argc, char **argv) { } } -#if SUPPORT_SINK if (connectToPort < 0 && uri.empty()) { fprintf(stderr, "You need to select either source host or uri.\n"); @@ -154,7 +146,6 @@ int main(int argc, char **argv) { } looper->start(true /* runOnCallingThread */); -#endif return 0; } |