blob: 009a3b1fb0eac6bc1903e848bcfc0091f08b92ae [file] [log] [blame]
/*
* Copyright (C) 2010 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 MY_TRANSMITTER_H_
#define MY_TRANSMITTER_H_
#include "ARTPConnection.h"
#include <arpa/inet.h>
#include <sys/socket.h>
#include <openssl/md5.h>
#include <media/stagefright/foundation/ADebug.h>
#include <media/stagefright/foundation/base64.h>
#include <media/stagefright/foundation/hexdump.h>
#ifdef ANDROID
#include "VideoSource.h"
#include <media/stagefright/OMXClient.h>
#include <media/stagefright/OMXCodec.h>
#endif
namespace android {
#define TRACK_SUFFIX "trackid=1"
#define PT 96
#define PT_STR "96"
#define USERNAME "bcast"
#define PASSWORD "test"
static int uniformRand(int limit) {
return ((double)rand() * limit) / RAND_MAX;
}
static bool GetAttribute(const char *s, const char *key, AString *value) {
value->clear();
size_t keyLen = strlen(key);
for (;;) {
const char *colonPos = strchr(s, ';');
size_t len =
(colonPos == NULL) ? strlen(s) : colonPos - s;
if (len >= keyLen + 1 && s[keyLen] == '=' && !strncmp(s, key, keyLen)) {
value->setTo(&s[keyLen + 1], len - keyLen - 1);
return true;
}
if (colonPos == NULL) {
return false;
}
s = colonPos + 1;
}
}
struct MyTransmitter : public AHandler {
MyTransmitter(const char *url, const sp<ALooper> &looper)
: mServerURL(url),
mLooper(looper),
mConn(new ARTSPConnection),
mConnected(false),
mAuthType(NONE),
mRTPSocket(-1),
mRTCPSocket(-1),
mSourceID(rand()),
mSeqNo(uniformRand(65536)),
mRTPTimeBase(rand()),
mNumSamplesSent(0),
mNumRTPSent(0),
mNumRTPOctetsSent(0),
mLastRTPTime(0),
mLastNTPTime(0) {
mStreamURL = mServerURL;
mStreamURL.append("/bazong.sdp");
mTrackURL = mStreamURL;
mTrackURL.append("/");
mTrackURL.append(TRACK_SUFFIX);
mLooper->registerHandler(this);
mLooper->registerHandler(mConn);
sp<AMessage> reply = new AMessage('conn', id());
mConn->connect(mServerURL.c_str(), reply);
#ifdef ANDROID
int width = 640;
int height = 480;
sp<MediaSource> source = new VideoSource(width, height);
sp<MetaData> encMeta = new MetaData;
encMeta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_VIDEO_AVC);
encMeta->setInt32(kKeyWidth, width);
encMeta->setInt32(kKeyHeight, height);
OMXClient client;
client.connect();
mEncoder = OMXCodec::Create(
client.interface(), encMeta,
true /* createEncoder */, source);
mEncoder->start();
MediaBuffer *buffer;
CHECK_EQ(mEncoder->read(&buffer), (status_t)OK);
CHECK(buffer != NULL);
makeH264SPropParamSets(buffer);
buffer->release();
buffer = NULL;
#endif
}
uint64_t ntpTime() {
struct timeval tv;
gettimeofday(&tv, NULL);
uint64_t nowUs = tv.tv_sec * 1000000ll + tv.tv_usec;
nowUs += ((70ll * 365 + 17) * 24) * 60 * 60 * 1000000ll;
uint64_t hi = nowUs / 1000000ll;
uint64_t lo = ((1ll << 32) * (nowUs % 1000000ll)) / 1000000ll;
return (hi << 32) | lo;
}
void issueAnnounce() {
AString sdp;
sdp = "v=0\r\n";
sdp.append("o=- ");
uint64_t ntp = ntpTime();
sdp.append(ntp);
sdp.append(" ");
sdp.append(ntp);
sdp.append(" IN IP4 127.0.0.0\r\n");
sdp.append(
"s=Sample\r\n"
"i=Playing around with ANNOUNCE\r\n"
"c=IN IP4 ");
struct in_addr addr;
addr.s_addr = htonl(mServerIP);
sdp.append(inet_ntoa(addr));
sdp.append(
"\r\n"
"t=0 0\r\n"
"a=range:npt=now-\r\n");
#ifdef ANDROID
sp<MetaData> meta = mEncoder->getFormat();
int32_t width, height;
CHECK(meta->findInt32(kKeyWidth, &width));
CHECK(meta->findInt32(kKeyHeight, &height));
sdp.append(
"m=video 0 RTP/AVP " PT_STR "\r\n"
"b=AS 320000\r\n"
"a=rtpmap:" PT_STR " H264/90000\r\n");
sdp.append("a=cliprect 0,0,");
sdp.append(height);
sdp.append(",");
sdp.append(width);
sdp.append("\r\n");
sdp.append(
"a=framesize:" PT_STR " ");
sdp.append(width);
sdp.append("-");
sdp.append(height);
sdp.append("\r\n");
sdp.append(
"a=fmtp:" PT_STR " profile-level-id=42C015;sprop-parameter-sets=");
sdp.append(mSeqParamSet);
sdp.append(",");
sdp.append(mPicParamSet);
sdp.append(";packetization-mode=1\r\n");
#else
sdp.append(
"m=audio 0 RTP/AVP " PT_STR "\r\n"
"a=rtpmap:" PT_STR " L8/8000/1\r\n");
#endif
sdp.append("a=control:" TRACK_SUFFIX "\r\n");
AString request;
request.append("ANNOUNCE ");
request.append(mStreamURL);
request.append(" RTSP/1.0\r\n");
addAuthentication(&request, "ANNOUNCE", mStreamURL.c_str());
request.append("Content-Type: application/sdp\r\n");
request.append("Content-Length: ");
request.append(sdp.size());
request.append("\r\n");
request.append("\r\n");
request.append(sdp);
sp<AMessage> reply = new AMessage('anno', id());
mConn->sendRequest(request.c_str(), reply);
}
void H(const AString &s, AString *out) {
out->clear();
MD5_CTX m;
MD5_Init(&m);
MD5_Update(&m, s.c_str(), s.size());
uint8_t key[16];
MD5_Final(key, &m);
for (size_t i = 0; i < 16; ++i) {
char nibble = key[i] >> 4;
if (nibble <= 9) {
nibble += '0';
} else {
nibble += 'a' - 10;
}
out->append(&nibble, 1);
nibble = key[i] & 0x0f;
if (nibble <= 9) {
nibble += '0';
} else {
nibble += 'a' - 10;
}
out->append(&nibble, 1);
}
}
void authenticate(const sp<ARTSPResponse> &response) {
ssize_t i = response->mHeaders.indexOfKey("www-authenticate");
CHECK_GE(i, 0);
AString value = response->mHeaders.valueAt(i);
if (!strncmp(value.c_str(), "Basic", 5)) {
mAuthType = BASIC;
} else {
CHECK(!strncmp(value.c_str(), "Digest", 6));
mAuthType = DIGEST;
i = value.find("nonce=");
CHECK_GE(i, 0);
CHECK_EQ(value.c_str()[i + 6], '\"');
ssize_t j = value.find("\"", i + 7);
CHECK_GE(j, 0);
mNonce.setTo(value, i + 7, j - i - 7);
}
issueAnnounce();
}
void addAuthentication(
AString *request, const char *method, const char *url) {
if (mAuthType == NONE) {
return;
}
if (mAuthType == BASIC) {
request->append("Authorization: Basic YmNhc3Q6dGVzdAo=\r\n");
return;
}
CHECK_EQ((int)mAuthType, (int)DIGEST);
AString A1;
A1.append(USERNAME);
A1.append(":");
A1.append("Streaming Server");
A1.append(":");
A1.append(PASSWORD);
AString A2;
A2.append(method);
A2.append(":");
A2.append(url);
AString HA1, HA2;
H(A1, &HA1);
H(A2, &HA2);
AString tmp;
tmp.append(HA1);
tmp.append(":");
tmp.append(mNonce);
tmp.append(":");
tmp.append(HA2);
AString digest;
H(tmp, &digest);
request->append("Authorization: Digest ");
request->append("nonce=\"");
request->append(mNonce);
request->append("\", ");
request->append("username=\"" USERNAME "\", ");
request->append("uri=\"");
request->append(url);
request->append("\", ");
request->append("response=\"");
request->append(digest);
request->append("\"");
request->append("\r\n");
}
virtual void onMessageReceived(const sp<AMessage> &msg) {
switch (msg->what()) {
case 'conn':
{
int32_t result;
CHECK(msg->findInt32("result", &result));
LOG(INFO) << "connection request completed with result "
<< result << " (" << strerror(-result) << ")";
if (result != OK) {
(new AMessage('quit', id()))->post();
break;
}
mConnected = true;
CHECK(msg->findInt32("server-ip", (int32_t *)&mServerIP));
issueAnnounce();
break;
}
case 'anno':
{
int32_t result;
CHECK(msg->findInt32("result", &result));
LOG(INFO) << "ANNOUNCE completed with result "
<< result << " (" << strerror(-result) << ")";
sp<RefBase> obj;
CHECK(msg->findObject("response", &obj));
sp<ARTSPResponse> response;
if (result == OK) {
response = static_cast<ARTSPResponse *>(obj.get());
CHECK(response != NULL);
if (response->mStatusCode == 401) {
if (mAuthType != NONE) {
LOG(INFO) << "FAILED to authenticate";
(new AMessage('quit', id()))->post();
break;
}
authenticate(response);
break;
}
}
if (result != OK || response->mStatusCode != 200) {
(new AMessage('quit', id()))->post();
break;
}
unsigned rtpPort;
ARTPConnection::MakePortPair(&mRTPSocket, &mRTCPSocket, &rtpPort);
// (new AMessage('poll', id()))->post();
AString request;
request.append("SETUP ");
request.append(mTrackURL);
request.append(" RTSP/1.0\r\n");
addAuthentication(&request, "SETUP", mTrackURL.c_str());
request.append("Transport: RTP/AVP;unicast;client_port=");
request.append(rtpPort);
request.append("-");
request.append(rtpPort + 1);
request.append(";mode=record\r\n");
request.append("\r\n");
sp<AMessage> reply = new AMessage('setu', id());
mConn->sendRequest(request.c_str(), reply);
break;
}
#if 0
case 'poll':
{
fd_set rs;
FD_ZERO(&rs);
FD_SET(mRTCPSocket, &rs);
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 0;
int res = select(mRTCPSocket + 1, &rs, NULL, NULL, &tv);
if (res == 1) {
sp<ABuffer> buffer = new ABuffer(65536);
ssize_t n = recv(mRTCPSocket, buffer->data(), buffer->size(), 0);
if (n <= 0) {
LOG(ERROR) << "recv returned " << n;
} else {
LOG(INFO) << "recv returned " << n << " bytes of data.";
hexdump(buffer->data(), n);
}
}
msg->post(50000);
break;
}
#endif
case 'setu':
{
int32_t result;
CHECK(msg->findInt32("result", &result));
LOG(INFO) << "SETUP completed with result "
<< result << " (" << strerror(-result) << ")";
sp<RefBase> obj;
CHECK(msg->findObject("response", &obj));
sp<ARTSPResponse> response;
if (result == OK) {
response = static_cast<ARTSPResponse *>(obj.get());
CHECK(response != NULL);
}
if (result != OK || response->mStatusCode != 200) {
(new AMessage('quit', id()))->post();
break;
}
ssize_t i = response->mHeaders.indexOfKey("session");
CHECK_GE(i, 0);
mSessionID = response->mHeaders.valueAt(i);
i = mSessionID.find(";");
if (i >= 0) {
// Remove options, i.e. ";timeout=90"
mSessionID.erase(i, mSessionID.size() - i);
}
i = response->mHeaders.indexOfKey("transport");
CHECK_GE(i, 0);
AString transport = response->mHeaders.valueAt(i);
LOG(INFO) << "transport = '" << transport << "'";
AString value;
CHECK(GetAttribute(transport.c_str(), "server_port", &value));
unsigned rtpPort, rtcpPort;
CHECK_EQ(sscanf(value.c_str(), "%u-%u", &rtpPort, &rtcpPort), 2);
CHECK(GetAttribute(transport.c_str(), "source", &value));
memset(mRemoteAddr.sin_zero, 0, sizeof(mRemoteAddr.sin_zero));
mRemoteAddr.sin_family = AF_INET;
mRemoteAddr.sin_addr.s_addr = inet_addr(value.c_str());
mRemoteAddr.sin_port = htons(rtpPort);
mRemoteRTCPAddr = mRemoteAddr;
mRemoteRTCPAddr.sin_port = htons(rtpPort + 1);
CHECK_EQ(0, connect(mRTPSocket,
(const struct sockaddr *)&mRemoteAddr,
sizeof(mRemoteAddr)));
CHECK_EQ(0, connect(mRTCPSocket,
(const struct sockaddr *)&mRemoteRTCPAddr,
sizeof(mRemoteRTCPAddr)));
uint32_t x = ntohl(mRemoteAddr.sin_addr.s_addr);
LOG(INFO) << "sending data to "
<< (x >> 24)
<< "."
<< ((x >> 16) & 0xff)
<< "."
<< ((x >> 8) & 0xff)
<< "."
<< (x & 0xff)
<< ":"
<< rtpPort;
AString request;
request.append("RECORD ");
request.append(mStreamURL);
request.append(" RTSP/1.0\r\n");
addAuthentication(&request, "RECORD", mStreamURL.c_str());
request.append("Session: ");
request.append(mSessionID);
request.append("\r\n");
request.append("\r\n");
sp<AMessage> reply = new AMessage('reco', id());
mConn->sendRequest(request.c_str(), reply);
break;
}
case 'reco':
{
int32_t result;
CHECK(msg->findInt32("result", &result));
LOG(INFO) << "RECORD completed with result "
<< result << " (" << strerror(-result) << ")";
sp<RefBase> obj;
CHECK(msg->findObject("response", &obj));
sp<ARTSPResponse> response;
if (result == OK) {
response = static_cast<ARTSPResponse *>(obj.get());
CHECK(response != NULL);
}
if (result != OK) {
(new AMessage('quit', id()))->post();
break;
}
(new AMessage('more', id()))->post();
(new AMessage('sr ', id()))->post();
(new AMessage('aliv', id()))->post(30000000ll);
break;
}
case 'aliv':
{
if (!mConnected) {
break;
}
AString request;
request.append("OPTIONS ");
request.append(mStreamURL);
request.append(" RTSP/1.0\r\n");
addAuthentication(&request, "RECORD", mStreamURL.c_str());
request.append("Session: ");
request.append(mSessionID);
request.append("\r\n");
request.append("\r\n");
sp<AMessage> reply = new AMessage('opts', id());
mConn->sendRequest(request.c_str(), reply);
break;
}
case 'opts':
{
int32_t result;
CHECK(msg->findInt32("result", &result));
LOG(INFO) << "OPTIONS completed with result "
<< result << " (" << strerror(-result) << ")";
if (!mConnected) {
break;
}
(new AMessage('aliv', id()))->post(30000000ll);
break;
}
case 'more':
{
if (!mConnected) {
break;
}
sp<ABuffer> buffer = new ABuffer(65536);
uint8_t *data = buffer->data();
data[0] = 0x80;
data[1] = (1 << 7) | PT; // M-bit
data[2] = (mSeqNo >> 8) & 0xff;
data[3] = mSeqNo & 0xff;
data[8] = mSourceID >> 24;
data[9] = (mSourceID >> 16) & 0xff;
data[10] = (mSourceID >> 8) & 0xff;
data[11] = mSourceID & 0xff;
#ifdef ANDROID
MediaBuffer *mediaBuf = NULL;
for (;;) {
CHECK_EQ(mEncoder->read(&mediaBuf), (status_t)OK);
if (mediaBuf->range_length() > 0) {
break;
}
mediaBuf->release();
mediaBuf = NULL;
}
int64_t timeUs;
CHECK(mediaBuf->meta_data()->findInt64(kKeyTime, &timeUs));
uint32_t rtpTime = mRTPTimeBase + (timeUs * 9 / 100ll);
const uint8_t *mediaData =
(const uint8_t *)mediaBuf->data() + mediaBuf->range_offset();
CHECK(!memcmp("\x00\x00\x00\x01", mediaData, 4));
CHECK_LE(mediaBuf->range_length() - 4 + 12, buffer->size());
memcpy(&data[12],
mediaData + 4, mediaBuf->range_length() - 4);
buffer->setRange(0, mediaBuf->range_length() - 4 + 12);
mediaBuf->release();
mediaBuf = NULL;
#else
uint32_t rtpTime = mRTPTimeBase + mNumRTPSent * 128;
memset(&data[12], 0, 128);
buffer->setRange(0, 12 + 128);
#endif
data[4] = rtpTime >> 24;
data[5] = (rtpTime >> 16) & 0xff;
data[6] = (rtpTime >> 8) & 0xff;
data[7] = rtpTime & 0xff;
ssize_t n = send(
mRTPSocket, data, buffer->size(), 0);
if (n < 0) {
LOG(ERROR) << "send failed (" << strerror(errno) << ")";
}
CHECK_EQ(n, (ssize_t)buffer->size());
++mSeqNo;
++mNumRTPSent;
mNumRTPOctetsSent += buffer->size() - 12;
mLastRTPTime = rtpTime;
mLastNTPTime = ntpTime();
#ifdef ANDROID
if (mNumRTPSent < 60 * 25) { // 60 secs worth
msg->post(40000);
#else
if (mNumRTPOctetsSent < 8000 * 60) {
msg->post(1000000ll * 128 / 8000);
#endif
} else {
LOG(INFO) << "That's enough, pausing.";
AString request;
request.append("PAUSE ");
request.append(mStreamURL);
request.append(" RTSP/1.0\r\n");
addAuthentication(&request, "PAUSE", mStreamURL.c_str());
request.append("Session: ");
request.append(mSessionID);
request.append("\r\n");
request.append("\r\n");
sp<AMessage> reply = new AMessage('paus', id());
mConn->sendRequest(request.c_str(), reply);
}
break;
}
case 'sr ':
{
if (!mConnected) {
break;
}
sp<ABuffer> buffer = new ABuffer(65536);
buffer->setRange(0, 0);
addSR(buffer);
addSDES(buffer);
uint8_t *data = buffer->data();
ssize_t n = send(
mRTCPSocket, data, buffer->size(), 0);
CHECK_EQ(n, (ssize_t)buffer->size());
msg->post(3000000);
break;
}
case 'paus':
{
int32_t result;
CHECK(msg->findInt32("result", &result));
LOG(INFO) << "PAUSE completed with result "
<< result << " (" << strerror(-result) << ")";
sp<RefBase> obj;
CHECK(msg->findObject("response", &obj));
sp<ARTSPResponse> response;
AString request;
request.append("TEARDOWN ");
request.append(mStreamURL);
request.append(" RTSP/1.0\r\n");
addAuthentication(&request, "TEARDOWN", mStreamURL.c_str());
request.append("Session: ");
request.append(mSessionID);
request.append("\r\n");
request.append("\r\n");
sp<AMessage> reply = new AMessage('tear', id());
mConn->sendRequest(request.c_str(), reply);
break;
}
case 'tear':
{
int32_t result;
CHECK(msg->findInt32("result", &result));
LOG(INFO) << "TEARDOWN completed with result "
<< result << " (" << strerror(-result) << ")";
sp<RefBase> obj;
CHECK(msg->findObject("response", &obj));
sp<ARTSPResponse> response;
if (result == OK) {
response = static_cast<ARTSPResponse *>(obj.get());
CHECK(response != NULL);
}
(new AMessage('quit', id()))->post();
break;
}
case 'disc':
{
LOG(INFO) << "disconnect completed";
mConnected = false;
(new AMessage('quit', id()))->post();
break;
}
case 'quit':
{
if (mConnected) {
mConn->disconnect(new AMessage('disc', id()));
break;
}
if (mRTPSocket >= 0) {
close(mRTPSocket);
mRTPSocket = -1;
}
if (mRTCPSocket >= 0) {
close(mRTCPSocket);
mRTCPSocket = -1;
}
#ifdef ANDROID
mEncoder->stop();
mEncoder.clear();
#endif
mLooper->stop();
break;
}
default:
TRESPASS();
}
}
protected:
virtual ~MyTransmitter() {
}
private:
enum AuthType {
NONE,
BASIC,
DIGEST
};
AString mServerURL;
AString mTrackURL;
AString mStreamURL;
sp<ALooper> mLooper;
sp<ARTSPConnection> mConn;
bool mConnected;
uint32_t mServerIP;
AuthType mAuthType;
AString mNonce;
AString mSessionID;
int mRTPSocket, mRTCPSocket;
uint32_t mSourceID;
uint32_t mSeqNo;
uint32_t mRTPTimeBase;
struct sockaddr_in mRemoteAddr;
struct sockaddr_in mRemoteRTCPAddr;
size_t mNumSamplesSent;
uint32_t mNumRTPSent;
uint32_t mNumRTPOctetsSent;
uint32_t mLastRTPTime;
uint64_t mLastNTPTime;
#ifdef ANDROID
sp<MediaSource> mEncoder;
AString mSeqParamSet;
AString mPicParamSet;
void makeH264SPropParamSets(MediaBuffer *buffer) {
static const char kStartCode[] = "\x00\x00\x00\x01";
const uint8_t *data =
(const uint8_t *)buffer->data() + buffer->range_offset();
size_t size = buffer->range_length();
CHECK_GE(size, 0u);
CHECK(!memcmp(kStartCode, data, 4));
data += 4;
size -= 4;
size_t startCodePos = 0;
while (startCodePos + 3 < size
&& memcmp(kStartCode, &data[startCodePos], 4)) {
++startCodePos;
}
CHECK_LT(startCodePos + 3, size);
encodeBase64(data, startCodePos, &mSeqParamSet);
encodeBase64(&data[startCodePos + 4], size - startCodePos - 4,
&mPicParamSet);
}
#endif
void addSR(const sp<ABuffer> &buffer) {
uint8_t *data = buffer->data() + buffer->size();
data[0] = 0x80 | 0;
data[1] = 200; // SR
data[2] = 0;
data[3] = 6;
data[4] = mSourceID >> 24;
data[5] = (mSourceID >> 16) & 0xff;
data[6] = (mSourceID >> 8) & 0xff;
data[7] = mSourceID & 0xff;
data[8] = mLastNTPTime >> (64 - 8);
data[9] = (mLastNTPTime >> (64 - 16)) & 0xff;
data[10] = (mLastNTPTime >> (64 - 24)) & 0xff;
data[11] = (mLastNTPTime >> 32) & 0xff;
data[12] = (mLastNTPTime >> 24) & 0xff;
data[13] = (mLastNTPTime >> 16) & 0xff;
data[14] = (mLastNTPTime >> 8) & 0xff;
data[15] = mLastNTPTime & 0xff;
data[16] = (mLastRTPTime >> 24) & 0xff;
data[17] = (mLastRTPTime >> 16) & 0xff;
data[18] = (mLastRTPTime >> 8) & 0xff;
data[19] = mLastRTPTime & 0xff;
data[20] = mNumRTPSent >> 24;
data[21] = (mNumRTPSent >> 16) & 0xff;
data[22] = (mNumRTPSent >> 8) & 0xff;
data[23] = mNumRTPSent & 0xff;
data[24] = mNumRTPOctetsSent >> 24;
data[25] = (mNumRTPOctetsSent >> 16) & 0xff;
data[26] = (mNumRTPOctetsSent >> 8) & 0xff;
data[27] = mNumRTPOctetsSent & 0xff;
buffer->setRange(buffer->offset(), buffer->size() + 28);
}
void addSDES(const sp<ABuffer> &buffer) {
uint8_t *data = buffer->data() + buffer->size();
data[0] = 0x80 | 1;
data[1] = 202; // SDES
data[4] = mSourceID >> 24;
data[5] = (mSourceID >> 16) & 0xff;
data[6] = (mSourceID >> 8) & 0xff;
data[7] = mSourceID & 0xff;
size_t offset = 8;
data[offset++] = 1; // CNAME
static const char *kCNAME = "andih@laptop";
data[offset++] = strlen(kCNAME);
memcpy(&data[offset], kCNAME, strlen(kCNAME));
offset += strlen(kCNAME);
data[offset++] = 7; // NOTE
static const char *kNOTE = "Hell's frozen over.";
data[offset++] = strlen(kNOTE);
memcpy(&data[offset], kNOTE, strlen(kNOTE));
offset += strlen(kNOTE);
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);
}
DISALLOW_EVIL_CONSTRUCTORS(MyTransmitter);
};
} // namespace android
#endif // MY_TRANSMITTER_H_