Add webrtc streamer

Bug: 141887532
Test: locally
Change-Id: I2a0d927470ba1f68cc94b94987a0e0de8b74333e
diff --git a/host/frontend/gcastv2/https/Android.bp b/host/frontend/gcastv2/https/Android.bp
new file mode 100644
index 0000000..a41fbe0
--- /dev/null
+++ b/host/frontend/gcastv2/https/Android.bp
@@ -0,0 +1,58 @@
+cc_library_static {
+    name: "libhttps",
+    host_supported: true,
+    srcs: [
+        "BaseConnection.cpp",
+        "BufferedSocket.cpp",
+        "ClientSocket.cpp",
+        "HTTPClientConnection.cpp",
+        "HTTPRequestResponse.cpp",
+        "HTTPServer.cpp",
+        "PlainSocket.cpp",
+        "SSLSocket.cpp",
+        "ServerSocket.cpp",
+        "WebSocketHandler.cpp",
+        "RunLoop.cpp",
+        "Support.cpp",
+    ],
+    target: {
+    	host: {
+	     cflags: [
+                "-DTARGET_ANDROID",
+            ],
+	},
+	android: {
+	    cflags: [
+                "-DTARGET_ANDROID_DEVICE",
+            ],
+	    },
+    },
+    static_libs: [
+        "libandroidglue",
+    ],
+    shared_libs: [
+        "libbase",
+        "libcrypto",
+        "libssl",
+    ],
+    local_include_dirs: ["include"],
+    export_include_dirs: ["include"],
+}
+
+cc_library_static {
+    name: "librunloop_device_nojava",
+    vendor_available: true,
+    srcs: [
+        "RunLoop.cpp",
+        "Support.cpp",
+    ],
+    cflags: [
+        "-DTARGET_ANDROID_DEVICE_NO_JAVA",
+    ],
+    shared_libs: [
+        "libbase",
+    ],
+    local_include_dirs: ["include"],
+    export_include_dirs: ["include"],
+}
+
diff --git a/host/frontend/gcastv2/https/BaseConnection.cpp b/host/frontend/gcastv2/https/BaseConnection.cpp
new file mode 100644
index 0000000..be13213
--- /dev/null
+++ b/host/frontend/gcastv2/https/BaseConnection.cpp
@@ -0,0 +1,109 @@
+#include <https/BaseConnection.h>
+
+#include <https/SafeCallbackable.h>
+#include <https/PlainSocket.h>
+
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/hexdump.h>
+
+BaseConnection::BaseConnection(std::shared_ptr<RunLoop> runLoop, int sock)
+    : mRunLoop(runLoop),
+      mSocket(std::make_unique<PlainSocket>(mRunLoop, sock)),
+      mInBufferLen(0),
+      mSendPending(false) {
+}
+
+void BaseConnection::run() {
+    receiveClientRequest();
+}
+
+void BaseConnection::receiveClientRequest() {
+    mSocket->postRecv(makeSafeCallback(this, &BaseConnection::onClientRequest));
+}
+
+void BaseConnection::onClientRequest() {
+    static constexpr size_t kMaxChunkSize = 8192;
+
+    mInBuffer.resize(mInBufferLen + kMaxChunkSize);
+
+    ssize_t n;
+    do {
+        n = mSocket->recv(&mInBuffer[mInBufferLen], kMaxChunkSize);
+    } while (n < 0 && errno == EINTR);
+
+    if (n <= 0) {
+        onDisconnect((n < 0) ? -errno : 0);
+        return;
+    }
+
+    mInBufferLen += static_cast<size_t>(n);
+
+    while (mInBufferLen > 0) {
+        n = processClientRequest(mInBuffer.data(), mInBufferLen);
+
+        if (n <= 0) {
+            break;
+        }
+
+        mInBuffer.erase(mInBuffer.begin(), mInBuffer.begin() + n);
+        mInBufferLen -= n;
+    }
+
+    if (n <= 0 && n != -EAGAIN && n != EWOULDBLOCK) {
+        onDisconnect(n);
+        return;
+    }
+
+    receiveClientRequest();
+}
+
+void BaseConnection::send(const void *_data, size_t size) {
+    const uint8_t *data = static_cast<const uint8_t *>(_data);
+    std::copy(data, data + size, std::back_inserter(mOutBuffer));
+
+    if (!mSendPending) {
+        mSendPending = true;
+        mSocket->postSend(
+                makeSafeCallback(this, &BaseConnection::sendOutputData));
+    }
+}
+
+void BaseConnection::sendOutputData() {
+    mSendPending = false;
+
+    const size_t size = mOutBuffer.size();
+    size_t offset = 0;
+
+    while (offset < size) {
+        ssize_t n = mSocket->send(mOutBuffer.data() + offset, size - offset);
+
+        if (n < 0) {
+            if (errno == EINTR) {
+                continue;
+            }
+
+            assert(!"Should not be here");
+        } else if (n == 0) {
+            // The remote seems gone, clear the output buffer and disconnect.
+            offset = size;
+            break;
+        }
+
+        offset += static_cast<size_t>(n);
+    }
+
+    mOutBuffer.erase(mOutBuffer.begin(), mOutBuffer.begin() + offset);
+
+    if (!mOutBuffer.empty()) {
+        mSendPending = true;
+
+        mSocket->postSend(
+                makeSafeCallback(this, &BaseConnection::sendOutputData));
+        return;
+    }
+}
+
+int BaseConnection::fd() const {
+    return mSocket->fd();
+}
+
diff --git a/host/frontend/gcastv2/https/BufferedSocket.cpp b/host/frontend/gcastv2/https/BufferedSocket.cpp
new file mode 100644
index 0000000..6257bd6
--- /dev/null
+++ b/host/frontend/gcastv2/https/BufferedSocket.cpp
@@ -0,0 +1,26 @@
+#include <https/BufferedSocket.h>
+
+#include <cassert>
+#include <sys/socket.h>
+#include <unistd.h>
+
+BufferedSocket::BufferedSocket(std::shared_ptr<RunLoop> rl, int sock)
+    : mRunLoop(rl),
+      mSock(sock) {
+    assert(mSock >= 0);
+}
+
+BufferedSocket::~BufferedSocket() {
+    mRunLoop->cancelSocket(mSock);
+
+    close(mSock);
+    mSock = -1;
+}
+
+int BufferedSocket::fd() const {
+    return mSock;
+}
+
+RunLoop *BufferedSocket::runLoop() {
+    return mRunLoop.get();
+}
diff --git a/host/frontend/gcastv2/https/ClientSocket.cpp b/host/frontend/gcastv2/https/ClientSocket.cpp
new file mode 100644
index 0000000..c4788ac
--- /dev/null
+++ b/host/frontend/gcastv2/https/ClientSocket.cpp
@@ -0,0 +1,241 @@
+#include <https/ClientSocket.h>
+
+#include <https/HTTPServer.h>
+#include <https/RunLoop.h>
+#include <https/SafeCallbackable.h>
+#include <https/ServerSocket.h>
+
+#include <media/stagefright/foundation/ADebug.h>
+
+#include <cstdlib>
+
+#if defined(TARGET_IOS) && __IPHONE_OS_VERSION_MIN_REQUIRED < 110000
+void *operator new(size_t size, std::align_val_t align) {
+    void *data;
+    posix_memalign(&data, static_cast<size_t>(align), size);
+
+    return data;
+}
+
+void operator delete(void *data, std::align_val_t align) {
+    free(data);
+}
+#endif
+
+ClientSocket::ClientSocket(
+        std::shared_ptr<RunLoop> rl,
+        HTTPServer *server,
+        ServerSocket *parent,
+        const sockaddr_in &addr,
+        int sock)
+    : mRunLoop(rl),
+      mServer(server),
+      mParent(parent),
+      mRemoteAddr(addr),
+      mInBufferLen(0),
+      mSendPending(false),
+      mDisconnecting(false) {
+    if (parent->transportType() == ServerSocket::TransportType::TLS) {
+        mImplSSL = std::make_shared<SSLSocket>(
+                mRunLoop,
+                sock,
+                *server->certificate_pem_path(),
+                *server->private_key_pem_path());
+    } else {
+        mImplPlain = std::make_shared<PlainSocket>(mRunLoop, sock);
+    }
+}
+
+void ClientSocket::run() {
+    getImpl()->postRecv(makeSafeCallback(this, &ClientSocket::handleIncomingData));
+}
+
+int ClientSocket::fd() const {
+    return getImpl()->fd();
+}
+
+void ClientSocket::setWebSocketHandler(
+        std::shared_ptr<WebSocketHandler> handler) {
+    mWebSocketHandler = handler;
+    mWebSocketHandler->setClientSocket(shared_from_this());
+}
+
+void ClientSocket::handleIncomingData() {
+    mInBuffer.resize(mInBufferLen + 1024);
+
+    ssize_t n;
+    do {
+        n = getImpl()->recv(mInBuffer.data() + mInBufferLen, 1024);
+    } while (n < 0 && errno == EINTR);
+
+    if (n == 0) {
+        if (errno == 0) {
+            // Don't process any data if there was an actual failure.
+            // This could be an authentication failure for example...
+            // We shouldn't trust anything the client says.
+            (void)handleRequest(true /* sawEOS */);
+        }
+
+        disconnect();
+        return;
+    } else if (n < 0) {
+        LOG(ERROR)
+            << "recv returned error "
+            << errno
+            << " ("
+            << strerror(errno)
+            << ")";
+
+        mParent->onClientSocketClosed(fd());
+        return;
+    }
+
+    mInBufferLen += static_cast<size_t>(n);
+    const bool closeConnection = handleRequest(false /* sawEOS */);
+
+    if (closeConnection) {
+        disconnect();
+    } else {
+        getImpl()->postRecv(
+                makeSafeCallback(this, &ClientSocket::handleIncomingData));
+    }
+}
+
+void ClientSocket::disconnect() {
+    if (mDisconnecting) {
+        return;
+    }
+
+    mDisconnecting = true;
+
+    finishDisconnect();
+}
+
+void ClientSocket::finishDisconnect() {
+    if (!mSendPending) {
+        // Our output queue may now be empty, but the underlying socket
+        // implementation may still buffer something that we need to flush
+        // first.
+        getImpl()->postFlush(
+                makeSafeCallback<ClientSocket>(this, [](ClientSocket *me) {
+                    me->mParent->onClientSocketClosed(me->fd());
+                }));
+    }
+}
+
+bool ClientSocket::handleRequest(bool isEOS) {
+    if (mWebSocketHandler) {
+        ssize_t n = mWebSocketHandler->handleRequest(
+                mInBuffer.data(), mInBufferLen, isEOS);
+
+        LOG(VERBOSE)
+            << "handleRequest returned "
+            << n
+            << " when called with "
+            << mInBufferLen
+            << ", eos="
+            << isEOS;
+
+        if (n > 0) {
+            mInBuffer.erase(mInBuffer.begin(), mInBuffer.begin() + n);
+            mInBufferLen -= n;
+        }
+
+        // NOTE: Do not return true, i.e. disconnect, if the json handler
+        // returns 0 bytes read, it simply means we need more data to continue.
+        return n < 0;
+    }
+
+    size_t len = mInBufferLen;
+
+    if (!isEOS) {
+        static const char kPattern[] = "\r\n\r\n";
+
+        // Don't count the trailing NUL.
+        static constexpr size_t kPatternLength = sizeof(kPattern) - 1;
+
+        size_t i = 0;
+        while (i + kPatternLength <= mInBufferLen
+                && memcmp(mInBuffer.data() + i, kPattern, kPatternLength)) {
+            ++i;
+        }
+
+        if (i + kPatternLength > mInBufferLen) {
+            return false;
+        }
+
+        // Found a match.
+        len = i + kPatternLength;
+    }
+
+    const bool closeConnection =
+        mServer->handleSingleRequest(this, mInBuffer.data(), len, isEOS);
+
+    mInBuffer.clear();
+    mInBufferLen = 0;
+
+    return closeConnection;
+}
+
+void ClientSocket::queueOutputData(const uint8_t *data, size_t size) {
+    std::copy(data, data + size, std::back_inserter(mOutBuffer));
+
+    if (!mSendPending) {
+        mSendPending = true;
+        getImpl()->postSend(makeSafeCallback(this, &ClientSocket::sendOutputData));
+    }
+}
+
+sockaddr_in ClientSocket::remoteAddr() const {
+    return mRemoteAddr;
+}
+
+void ClientSocket::queueResponse(
+        const std::string &response, const std::string &body) {
+    std::copy(response.begin(), response.end(), std::back_inserter(mOutBuffer));
+    std::copy(body.begin(), body.end(), std::back_inserter(mOutBuffer));
+
+    if (!mSendPending) {
+        mSendPending = true;
+        getImpl()->postSend(makeSafeCallback(this, &ClientSocket::sendOutputData));
+    }
+}
+
+void ClientSocket::sendOutputData() {
+    mSendPending = false;
+
+    const size_t size = mOutBuffer.size();
+    size_t offset = 0;
+
+    while (offset < size) {
+        ssize_t n = getImpl()->send(mOutBuffer.data() + offset, size - offset);
+
+        if (n < 0) {
+            if (errno == EINTR) {
+                continue;
+            }
+
+            assert(!"Should not be here");
+        } else if (n == 0) {
+            // The remote seems gone, clear the output buffer and disconnect.
+            offset = size;
+            mDisconnecting = true;
+            break;
+        }
+
+        offset += static_cast<size_t>(n);
+    }
+
+    mOutBuffer.erase(mOutBuffer.begin(), mOutBuffer.begin() + offset);
+
+    if (!mOutBuffer.empty()) {
+        mSendPending = true;
+        getImpl()->postSend(makeSafeCallback(this, &ClientSocket::sendOutputData));
+        return;
+    }
+
+    if (mDisconnecting) {
+        finishDisconnect();
+    }
+}
+
diff --git a/host/frontend/gcastv2/https/HTTPClientConnection.cpp b/host/frontend/gcastv2/https/HTTPClientConnection.cpp
new file mode 100644
index 0000000..9b3a68a
--- /dev/null
+++ b/host/frontend/gcastv2/https/HTTPClientConnection.cpp
@@ -0,0 +1,276 @@
+#include <https/HTTPClientConnection.h>
+
+#include <https/HTTPRequestResponse.h>
+#include <https/PlainSocket.h>
+#include <https/RunLoop.h>
+#include <https/SafeCallbackable.h>
+#include <https/SSLSocket.h>
+
+#include <https/Support.h>
+
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/JSONObject.h>
+
+#include <arpa/inet.h>
+#include <cerrno>
+#include <netinet/in.h>
+#include <unistd.h>
+
+using namespace android;
+
+HTTPClientConnection::HTTPClientConnection(
+        std::shared_ptr<RunLoop> rl,
+        std::shared_ptr<WebSocketHandler> webSocketHandler,
+        std::string_view path,
+        ServerSocket::TransportType transportType,
+        const std::optional<std::string> &trusted_pem_path)
+    : mInitCheck(-ENODEV),
+      mRunLoop(rl),
+      mWebSocketHandler(webSocketHandler),
+      mPath(path),
+      mTransportType(transportType),
+      mSendPending(false),
+      mInBufferLen(0),
+      mWebSocketMode(false) {
+    int sock;
+
+    sock = socket(PF_INET, SOCK_STREAM, 0);
+    if (sock < 0) {
+        mInitCheck = -errno;
+        goto bail;
+    }
+
+    makeFdNonblocking(sock);
+
+    if (mTransportType == ServerSocket::TransportType::TLS) {
+        CHECK(trusted_pem_path.has_value());
+
+        mImpl = std::make_shared<SSLSocket>(
+                mRunLoop, sock, 0 /* flags */, *trusted_pem_path);
+    } else {
+        mImpl = std::make_shared<PlainSocket>(mRunLoop, sock);
+    }
+
+    mInitCheck = 0;
+    return;
+
+bail:
+    ;
+}
+
+int HTTPClientConnection::initCheck() const {
+    return mInitCheck;
+}
+
+int HTTPClientConnection::connect(const char *host, uint16_t port) {
+    if (mInitCheck < 0) {
+        return mInitCheck;
+    }
+
+    sockaddr_in addr;
+    memset(addr.sin_zero, 0, sizeof(addr.sin_zero));
+    addr.sin_family = AF_INET;
+    addr.sin_port = htons(port);
+    addr.sin_addr.s_addr = inet_addr(host);
+
+    mRemoteAddr = addr;
+
+    int res = ::connect(
+            mImpl->fd(), reinterpret_cast<sockaddr *>(&addr), sizeof(addr));
+
+    if (res < 0 && errno != EINPROGRESS) {
+        return -errno;
+    }
+
+    mImpl->postSend(makeSafeCallback(this, &HTTPClientConnection::sendRequest));
+
+    return 0;
+}
+
+void HTTPClientConnection::sendRequest() {
+    std::string request;
+    request =
+        "GET " + mPath + " HTTP/1.1\r\n"
+        "Connection: Upgrade\r\n"
+        "Upgrade: websocket\r\n"
+        "Sec-WebSocket-Version: 13\r\n"
+        "Sec-WebSocket-Key: foobar\r\n"
+        "\r\n";
+
+    CHECK(mRunLoop->isCurrentThread());
+    std::copy(request.begin(), request.end(), std::back_inserter(mOutBuffer));
+
+    if (!mSendPending) {
+        mSendPending = true;
+        mImpl->postSend(
+                makeSafeCallback(this, &HTTPClientConnection::sendOutputData));
+    }
+
+    mImpl->postRecv(
+            makeSafeCallback(this, &HTTPClientConnection::receiveResponse));
+}
+
+void HTTPClientConnection::receiveResponse() {
+    mInBuffer.resize(mInBufferLen + 1024);
+
+    ssize_t n;
+    do {
+        n = mImpl->recv(mInBuffer.data() + mInBufferLen, 1024);
+    } while (n < 0 && errno == EINTR);
+
+    if (n == 0) {
+        (void)handleResponse(true /* isEOS */);
+        return;
+    } else if (n < 0) {
+        LOG(ERROR) << "recv returned error '" << strerror(errno) << "'.";
+        return;
+    }
+
+    mInBufferLen += static_cast<size_t>(n);
+
+    if (!handleResponse(false /* isEOS */)) {
+        mImpl->postRecv(
+                makeSafeCallback(this, &HTTPClientConnection::receiveResponse));
+    }
+}
+
+bool HTTPClientConnection::handleResponse(bool isEOS) {
+    if (mWebSocketMode) {
+        ssize_t n = mWebSocketHandler->handleRequest(
+                mInBuffer.data(), mInBufferLen, isEOS);
+
+        if (n > 0) {
+            mInBuffer.erase(mInBuffer.begin(), mInBuffer.begin() + n);
+            mInBufferLen -= n;
+        }
+
+        return n <= 0;
+    }
+
+    size_t len = mInBufferLen;
+
+    if (!isEOS) {
+        static const char kPattern[] = "\r\n\r\n";
+
+        // Don't count the trailing NUL.
+        static constexpr size_t kPatternLength = sizeof(kPattern) - 1;
+
+        size_t i = 0;
+        while (i + kPatternLength <= mInBufferLen
+                && memcmp(mInBuffer.data() + i, kPattern, kPatternLength)) {
+            ++i;
+        }
+
+        if (i + kPatternLength > mInBufferLen) {
+            return false;
+        }
+
+        // Found a match.
+        len = i + kPatternLength;
+    }
+
+    HTTPResponse response;
+    if (response.setTo(mInBuffer.data(), len) < 0) {
+        LOG(ERROR) << "failed to get valid server response.";
+
+        mInBuffer.clear();
+        mInBufferLen = 0;
+
+        return true;
+    } else {
+        LOG(INFO)
+            << "got response: "
+            << response.getVersion()
+            << ", "
+            << response.getStatusCode()
+            << ", "
+            << response.getStatusMessage();
+
+        hexdump(mInBuffer.data(), len);
+
+        mInBuffer.erase(mInBuffer.begin(), mInBuffer.begin() + len);
+        mInBufferLen -= len;
+
+        size_t contentLength = response.getContentLength();
+        LOG(VERBOSE) << "contentLength = " << contentLength;
+        assert(mInBufferLen >= contentLength);
+
+        hexdump(mInBuffer.data(), contentLength);
+        mInBuffer.clear();
+
+        if (response.getStatusCode() == 101) {
+            mWebSocketMode = true;
+
+            mWebSocketHandler->setOutputCallback(
+                    mRemoteAddr,
+                    [this](const uint8_t *data, size_t size) {
+                        queueOutputData(data, size);
+                    });
+
+            using android::sp;
+            using android::JSONObject;
+            sp<JSONObject> jsonRequest = new JSONObject;
+            jsonRequest->setString("message", "Hello, world!");
+
+            const std::string msg = jsonRequest->toString();
+            mWebSocketHandler->sendMessage(msg.c_str(), msg.size());
+
+            return false;
+        }
+    }
+
+    return true;
+}
+
+void HTTPClientConnection::queueOutputData(const uint8_t *data, size_t size) {
+    CHECK(mRunLoop->isCurrentThread());
+    std::copy(data, &data[size], std::back_inserter(mOutBuffer));
+
+    if (!mSendPending) {
+        mSendPending = true;
+        mImpl->postSend(
+                makeSafeCallback(this, &HTTPClientConnection::sendOutputData));
+    }
+}
+
+void HTTPClientConnection::sendOutputData() {
+    mSendPending = false;
+
+    const size_t size = mOutBuffer.size();
+    size_t offset = 0;
+
+    while (offset < size) {
+        ssize_t n = mImpl->send(mOutBuffer.data() + offset, size - offset);
+
+        if (n < 0) {
+            if (errno == EINTR) {
+                continue;
+            }
+
+            if (errno == EAGAIN) {
+                break;
+            }
+
+            // The remote is gone (due to error), clear the output buffer and disconnect.
+            offset = size;
+            break;
+        } else if (n == 0) {
+            // The remote seems gone, clear the output buffer and disconnect.
+            offset = size;
+            break;
+        }
+
+        offset += static_cast<size_t>(n);
+    }
+
+    mOutBuffer.erase(mOutBuffer.begin(), mOutBuffer.begin() + offset);
+
+    if (!mOutBuffer.empty()) {
+        mSendPending = true;
+        mImpl->postSend(
+                makeSafeCallback(this, &HTTPClientConnection::sendOutputData));
+
+        return;
+    }
+}
+
diff --git a/host/frontend/gcastv2/https/HTTPRequestResponse.cpp b/host/frontend/gcastv2/https/HTTPRequestResponse.cpp
new file mode 100644
index 0000000..b61d436
--- /dev/null
+++ b/host/frontend/gcastv2/https/HTTPRequestResponse.cpp
@@ -0,0 +1,158 @@
+#include <https/HTTPRequestResponse.h>
+
+#include <cerrno>
+#include <iostream>
+#include <regex>
+
+HTTPRequestResponse::HTTPRequestResponse()
+    : mInitCheck(-ENODEV),
+      mContentLength(0) {
+}
+
+int HTTPRequestResponse::setTo(const uint8_t *data, size_t size) {
+    mInitCheck = -EINVAL;
+
+    bool sawEmptyLine = false;
+
+    size_t start = 0;
+    while (start < size) {
+        size_t end = start;
+        while (end + 1 < size && memcmp(&data[end], "\r\n", 2)) {
+            ++end;
+        }
+
+        if ((end + 1) == size) {
+            return mInitCheck;
+        }
+
+        std::string line(
+                reinterpret_cast<const char *>(&data[start]), end - start);
+
+        if (start == 0) {
+            // Parse the request/response line.
+
+            if (!parseRequestResponseLine(line)) {
+                return mInitCheck;
+            }
+
+        } else if (end > start) {
+            std::regex re("([a-zA-Z0-9-]+): (.*)");
+            std::smatch match;
+
+            if (!std::regex_match(line, match, re)) {
+                return mInitCheck;
+            }
+
+            auto key = match[1];
+            auto value = match[2];
+            mHeaders[key] = value;
+        }
+
+        sawEmptyLine = line.empty();
+
+        start = end + 2;
+    }
+
+    if (!sawEmptyLine) {
+        return mInitCheck;
+    }
+
+    std::string stringValue;
+    if (getHeaderField("Content-Length", &stringValue)) {
+        char *end;
+        unsigned long value = strtoul(stringValue.c_str(), &end, 10);
+
+        if (end == stringValue.c_str() || *end != '\0') {
+            return mInitCheck;
+        }
+
+        mContentLength = value;
+    }
+
+    mInitCheck = 0;
+    return mInitCheck;
+}
+
+int HTTPRequestResponse::initCheck() const {
+    return mInitCheck;
+}
+
+bool HTTPRequestResponse::getHeaderField(
+        std::string_view key, std::string *value) const {
+    auto it = mHeaders.find(std::string(key));
+
+    if (it != mHeaders.end()) {
+        *value = it->second;
+        return true;
+    }
+
+    return false;
+}
+
+size_t HTTPRequestResponse::getContentLength() const {
+    return mContentLength;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::string HTTPRequest::getMethod() const {
+    return mMethod;
+}
+
+std::string HTTPRequest::getPath() const {
+    return mPath;
+}
+
+std::string HTTPRequest::getVersion() const {
+    return mVersion;
+}
+
+bool HTTPRequest::parseRequestResponseLine(const std::string &line) {
+    std::regex re("(GET|HEAD) ([a-zA-Z_/.0-9?&=]+) (HTTP/1\\.1)");
+    std::smatch match;
+
+    if (!std::regex_match(line, match, re)) {
+        return false;
+    }
+
+    mMethod = match[1];
+    mPath = match[2];
+    mVersion = match[3];
+
+    return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::string HTTPResponse::getVersion() const {
+    return mVersion;
+}
+
+int32_t HTTPResponse::getStatusCode() const {
+    return mStatusCode;
+}
+
+std::string HTTPResponse::getStatusMessage() const {
+    return mStatusMessage;
+}
+
+bool HTTPResponse::parseRequestResponseLine(const std::string &line) {
+    std::regex re(
+            "(HTTP/1\\.1) ([1-9][0-9][0-9]) ([a-zA-Z _0-9.]+)");
+
+    std::smatch match;
+
+    if (!std::regex_match(line, match, re)) {
+        return false;
+    }
+
+    mVersion = match[1];
+    std::string statusString = match[2];
+    mStatusMessage = match[3];
+
+    mStatusCode =
+        static_cast<int32_t>(strtol(statusString.c_str(), nullptr, 10));
+
+    return true;
+}
+
diff --git a/host/frontend/gcastv2/https/HTTPServer.cpp b/host/frontend/gcastv2/https/HTTPServer.cpp
new file mode 100644
index 0000000..bff693d
--- /dev/null
+++ b/host/frontend/gcastv2/https/HTTPServer.cpp
@@ -0,0 +1,388 @@
+#include <https/HTTPServer.h>
+
+#include <https/ClientSocket.h>
+#include <https/HTTPRequestResponse.h>
+#include <https/Support.h>
+
+#include <media/stagefright/foundation/base64.h>
+
+#include <iostream>
+#include <map>
+#include <string>
+
+#if defined(TARGET_MAC) || defined(TARGET_IOS)
+#include <CommonCrypto/CommonDigest.h>
+#endif
+
+#if defined(TARGET_ANDROID) || defined(TARGET_ANDROID_DEVICE)
+#include <openssl/sha.h>
+
+#define CC_SHA1_CTX     SHA_CTX
+#define CC_SHA1_Init    SHA1_Init
+#define CC_SHA1_Update  SHA1_Update
+#define CC_SHA1_Final   SHA1_Final
+#define CC_LONG         size_t
+#endif
+
+HTTPServer::HTTPServer(
+        std::shared_ptr<RunLoop> runLoop,
+        const char *iface,
+        uint16_t port,
+        ServerSocket::TransportType transportType,
+        const std::optional<std::string> &certificate_pem_path,
+        const std::optional<std::string> &private_key_pem_path)
+    : mRunLoop(runLoop),
+      mLocalPort(port),
+      mSocketTLS(
+              std::make_shared<ServerSocket>(
+                  this,
+                  transportType,
+                  iface ? iface : "0.0.0.0",
+                  port,
+                  certificate_pem_path,
+                  private_key_pem_path)) {
+    CHECK(mSocketTLS->initCheck() == 0);
+}
+
+uint16_t HTTPServer::getLocalPort() const {
+    return mLocalPort;
+}
+
+void HTTPServer::run() {
+    mSocketTLS->run(mRunLoop);
+}
+
+bool HTTPServer::handleSingleRequest(
+        ClientSocket *clientSocket,
+        const uint8_t *data,
+        size_t size,
+        bool isEOS) {
+    (void)isEOS;
+
+    static const std::unordered_map<int32_t, std::string> kStatusMessage {
+        { 101, "Switching Protocols" },
+        { 200, "OK" },
+        { 400, "Bad Request" },
+        { 404, "Not Found" },
+        { 405, "Method Not Allowed" },
+        { 503, "Service Unavailable" },
+        { 505, "HTTP Version Not Supported" },
+    };
+
+#if 0
+    LOG(INFO)
+        << "handleSingleRequest on socket " << clientSocket->fd() << std::endl;
+
+    hexdump(data, size);
+#endif
+
+    HTTPRequest request;
+    request.setTo(data, size);
+
+    int32_t httpResultCode;
+    std::string body;
+    std::unordered_map<std::string, std::string> responseHeaders;
+
+    if (request.initCheck() < 0) {
+        httpResultCode = 400;  // Bad Request
+    } else if (request.getMethod() != "GET") {
+        httpResultCode = 405;  // Method Not Allowed
+    } else if (request.getVersion() != "HTTP/1.1") {
+        httpResultCode = 505;  // HTTP Version Not Supported
+    } else {
+        httpResultCode = 404;
+
+        auto path = request.getPath();
+
+        std::string query;
+
+        auto separatorPos = path.find("?");
+        if (separatorPos != std::string::npos) {
+            query = path.substr(separatorPos);
+            path.erase(separatorPos);
+        }
+
+        if (path == "/") { path = "/index.html"; }
+
+        bool done = false;
+
+        {
+            std::lock_guard autoLock(mContentLock);
+
+            auto it = mStaticFiles.find(path);
+
+            if (it != mStaticFiles.end()) {
+                handleStaticFileRequest(
+                        it->second,
+                        request,
+                        &httpResultCode,
+                        &responseHeaders,
+                        &body);
+
+                done = true;
+            }
+        }
+
+        if (!done) {
+            std::lock_guard autoLock(mContentLock);
+
+            auto it = mWebSocketHandlerFactories.find(path);
+
+            if (it != mWebSocketHandlerFactories.end()) {
+                handleWebSocketRequest(
+                        clientSocket,
+                        it->second,
+                        request,
+                        &httpResultCode,
+                        &responseHeaders,
+                        &body);
+
+                done = true;
+            }
+        }
+
+        const auto remoteAddr = clientSocket->remoteAddr();
+        uint32_t ip = ntohl(remoteAddr.sin_addr.s_addr);
+
+        LOG(INFO)
+            << (ip >> 24)
+            << "."
+            << ((ip >> 16) & 0xff)
+            << "."
+            << ((ip >> 8) & 0xff)
+            << "."
+            << (ip & 0xff)
+            << ":"
+            << ntohs(remoteAddr.sin_port)
+            << " "
+            << httpResultCode << " \"" << path << "\"";
+    }
+
+    const std::string status =
+        std::to_string(httpResultCode)
+            + " "
+            + kStatusMessage.find(httpResultCode)->second;
+
+    bool closeConnection = false;
+
+    if (httpResultCode != 200 && httpResultCode != 101) {
+        body = "<h1>" + status + "</h1>";
+
+        responseHeaders["Connection"] = "close";
+        responseHeaders["Content-Type"] = "text/html";
+
+        closeConnection = true;
+    }
+
+    std::string value;
+    if (request.getHeaderField("Connection", &value) && value == "close") {
+        LOG(VERBOSE) << "Closing connection per client's request.";
+        closeConnection = true;
+    }
+
+    responseHeaders["Content-Length"] = std::to_string(body.size());
+
+    if (closeConnection) {
+        responseHeaders["Connection"] = "close";
+    }
+
+    std::string response;
+    response = "HTTP/1.1 " + status + "\r\n";
+
+    for (const auto &pair : responseHeaders) {
+        response += pair.first + ": " + pair.second + "\r\n";
+    }
+
+    response += "\r\n";
+
+    clientSocket->queueResponse(response, body);
+
+    return closeConnection;
+}
+
+void HTTPServer::addStaticFile(
+        const char *at, const char *path, std::optional<std::string> mimeType) {
+    std::lock_guard autoLock(mContentLock);
+    mStaticFiles[at] = { path, mimeType };
+}
+
+void HTTPServer::addStaticContent(
+        const char *at,
+        const void *_data,
+        size_t size,
+        std::optional<std::string> mimeType) {
+    if (!mimeType) {
+        // Note: unlike for static, file-based content, we guess the mime type
+        // based on the path we're mapping the content at, not the path it's
+        // originating from (since we don't know that for memory based content).
+        mimeType = GuessMimeType(at);
+    }
+
+    auto data = static_cast<const uint8_t *>(_data);
+
+    std::lock_guard autoLock(mContentLock);
+    mStaticFiles[at] = { std::vector<uint8_t>(data, data + size), mimeType };
+}
+
+void HTTPServer::addWebSocketHandlerFactory(
+        const char *at, WebSocketHandlerFactory factory) {
+    std::lock_guard autoLock(mContentLock);
+    mWebSocketHandlerFactories[at] = factory;
+}
+
+void HTTPServer::handleWebSocketRequest(
+        ClientSocket *clientSocket,
+        WebSocketHandlerFactory factory,
+        const HTTPRequest &request,
+        int32_t *httpResultCode,
+        std::unordered_map<std::string, std::string> *responseHeaders,
+        std::string *body) {
+    (void)body;
+
+    auto [status, handler] = factory();
+
+    if (status != 0 || !handler) {
+        *httpResultCode = 503;  // Service unavailable.
+        return;
+    }
+
+    *httpResultCode = 400;
+
+    std::string value;
+    if (!request.getHeaderField("Connection", &value)
+            || (value != "Upgrade" && value != "keep-alive, Upgrade")) {
+        return;
+    }
+
+    if (!request.getHeaderField("Upgrade", &value) || value != "websocket") {
+        return;
+    }
+
+    if (!request.getHeaderField("Sec-WebSocket-Version", &value)) {
+        return;
+    }
+
+    char *end;
+    long version = strtol(value.c_str(), &end, 10);
+
+    if (end == value.c_str() || *end != '\0' || version < 13) {
+        return;
+    }
+
+    if (!request.getHeaderField("Sec-WebSocket-Key", &value)) {
+        return;
+    }
+
+    *httpResultCode = 101;
+
+    (*responseHeaders)["Connection"] = "Upgrade";
+    (*responseHeaders)["Upgrade"] = "websocket";
+
+    std::string tmp = value;
+    tmp += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+    CC_SHA1_CTX ctx;
+    int res = CC_SHA1_Init(&ctx);
+    CHECK_EQ(res, 1);
+
+    res = CC_SHA1_Update(
+                &ctx, tmp.c_str(), static_cast<CC_LONG>(tmp.size()));
+
+    CHECK_EQ(res, 1);
+
+    unsigned char digest[20];  // 160 bit
+    res = CC_SHA1_Final(digest, &ctx);
+    CHECK_EQ(res, 1);
+
+    std::string acceptKey;
+    android::encodeBase64(digest, sizeof(digest), &acceptKey);
+
+    (*responseHeaders)["Sec-WebSocket-Accept"] = acceptKey;
+
+    clientSocket->setWebSocketHandler(handler);
+}
+
+void HTTPServer::handleStaticFileRequest(
+        const StaticFileInfo &info,
+        const HTTPRequest &request,
+        int32_t *httpResultCode,
+        std::unordered_map<std::string, std::string> *responseHeaders,
+        std::string *body) {
+    (void)request;
+
+    if (std::holds_alternative<std::string>(info.mPathOrContent)) {
+        const auto &path = std::get<std::string>(info.mPathOrContent);
+
+        std::unique_ptr<FILE, std::function<int(FILE *)>> file(
+                fopen(path.c_str(), "r"),
+                fclose);
+
+        if (!file) {
+            *httpResultCode = 404;
+            return;
+        }
+
+        fseek(file.get(), 0, SEEK_END);
+        long fileSize = ftell(file.get());
+        fseek(file.get(), 0, SEEK_SET);
+
+        (*responseHeaders)["Content-Length"] = std::to_string(fileSize);
+
+        if (info.mMimeType) {
+            (*responseHeaders)["Content-Type"] = *info.mMimeType;
+        } else {
+            (*responseHeaders)["Content-Type"] = GuessMimeType(path);
+        }
+
+        while (!feof(file.get())) {
+            char buffer[1024];
+            auto n = fread(buffer, 1, sizeof(buffer), file.get());
+
+            body->append(buffer, n);
+        }
+    } else {
+        CHECK(std::holds_alternative<std::vector<uint8_t>>(
+                    info.mPathOrContent));
+
+        const auto &content =
+            std::get<std::vector<uint8_t>>(info.mPathOrContent);
+
+        body->append(content.begin(), content.end());
+
+        (*responseHeaders)["Content-Length"] = std::to_string(content.size());
+    }
+
+    *httpResultCode = 200;
+}
+
+// static
+std::string HTTPServer::GuessMimeType(const std::string &path) {
+    auto dotPos = path.rfind('.');
+    if (dotPos != std::string::npos) {
+        auto extension = std::string(path, dotPos + 1);
+
+        static std::unordered_map<std::string, std::string>
+            sMimeTypeByExtension {
+
+            { "html", "text/html" },
+            { "htm", "text/html" },
+            { "css", "text/css" },
+            { "js", "text/javascript" },
+        };
+
+        auto it = sMimeTypeByExtension.find(extension);
+        if (it != sMimeTypeByExtension.end()) {
+            return it->second;
+        }
+    }
+
+    return "application/octet-stream";
+}
+
+std::optional<std::string> HTTPServer::certificate_pem_path() const {
+    return mSocketTLS->certificate_pem_path();
+}
+
+std::optional<std::string> HTTPServer::private_key_pem_path() const {
+    return mSocketTLS->private_key_pem_path();
+}
diff --git a/host/frontend/gcastv2/https/PlainSocket.cpp b/host/frontend/gcastv2/https/PlainSocket.cpp
new file mode 100644
index 0000000..583567e
--- /dev/null
+++ b/host/frontend/gcastv2/https/PlainSocket.cpp
@@ -0,0 +1,38 @@
+#include <https/PlainSocket.h>
+
+#include <sys/socket.h>
+
+PlainSocket::PlainSocket(std::shared_ptr<RunLoop> rl, int sock)
+    : BufferedSocket(rl, sock) {
+}
+
+void PlainSocket::postRecv(RunLoop::AsyncFunction fn) {
+    runLoop()->postSocketRecv(fd(), fn);
+}
+
+void PlainSocket::postSend(RunLoop::AsyncFunction fn) {
+    runLoop()->postSocketSend(fd(), fn);
+}
+
+ssize_t PlainSocket::recvfrom(
+        void *data,
+        size_t size,
+        sockaddr *address,
+        socklen_t *addressLen) {
+    return ::recvfrom(fd(), data, size, 0, address, addressLen);
+}
+
+ssize_t PlainSocket::sendto(
+        const void *data,
+        size_t size,
+        const sockaddr *addr,
+        socklen_t addrLen) {
+    if (!addr) {
+        return ::send(fd(), data, size, 0);
+    }
+    return ::sendto(fd(), data, size, 0, addr, addrLen);
+}
+
+void PlainSocket::postFlush(RunLoop::AsyncFunction fn) {
+    fn();
+}
diff --git a/host/frontend/gcastv2/https/README b/host/frontend/gcastv2/https/README
new file mode 100644
index 0000000..49fefc6
--- /dev/null
+++ b/host/frontend/gcastv2/https/README
@@ -0,0 +1,5 @@
+Test using
+
+    curl --cacert certs/server.crt https://localhost:443/
+
+
diff --git a/host/frontend/gcastv2/https/RunLoop.cpp b/host/frontend/gcastv2/https/RunLoop.cpp
new file mode 100644
index 0000000..e49d598
--- /dev/null
+++ b/host/frontend/gcastv2/https/RunLoop.cpp
@@ -0,0 +1,412 @@
+#include <https/RunLoop.h>
+
+#include <https/Support.h>
+
+#ifdef TARGET_ANDROID_DEVICE
+#include <helpers/JavaThread.h>
+#endif
+
+#ifdef TARGET_ANDROID_DEVICE_NO_JAVA
+#include <android-base/logging.h>
+#else
+#include <media/stagefright/foundation/ADebug.h>
+#endif
+
+#include <cstring>
+#include <fcntl.h>
+#include <iostream>
+#include <unistd.h>
+
+bool RunLoop::QueueElem::operator<=(const QueueElem &other) const {
+    if (mWhen) {
+        if (other.mWhen) {
+            return mWhen <= other.mWhen;
+        }
+
+        return false;
+    }
+
+    if (other.mWhen) {
+        return true;
+    }
+
+    // This ensures that two events posted without a trigger time are queued in
+    // the order they were post()ed in.
+    return true;
+}
+
+RunLoop::RunLoop()
+    : mDone(false),
+      mPThread(0),
+      mNextToken(1) {
+    int res = pipe(mControlFds);
+    CHECK_GE(res, 0);
+
+    makeFdNonblocking(mControlFds[0]);
+}
+
+RunLoop::RunLoop(std::string_view name)
+    : RunLoop() {
+    mName = name;
+
+#ifdef TARGET_ANDROID_DEVICE
+    mThread = android::createJavaThread([this]{ run(); });
+#else
+    mThread = std::thread([this]{ run(); });
+#endif
+}
+
+RunLoop::~RunLoop() {
+    stop();
+
+    close(mControlFds[1]);
+    mControlFds[1] = -1;
+
+    close(mControlFds[0]);
+    mControlFds[0] = -1;
+}
+
+void RunLoop::stop() {
+    mDone = true;
+    interrupt();
+
+    if (mThread.joinable()) {
+        mThread.join();
+    }
+}
+
+RunLoop::Token RunLoop::post(AsyncFunction fn) {
+    CHECK(fn != nullptr);
+
+    auto token = mNextToken++;
+    insert({ std::nullopt, fn, token });
+
+    return token;
+}
+
+RunLoop::Token RunLoop::postWithDelay(
+        std::chrono::steady_clock::duration delay, AsyncFunction fn) {
+    CHECK(fn != nullptr);
+
+    auto token = mNextToken++;
+    insert({ std::chrono::steady_clock::now() + delay, fn, token });
+
+    return token;
+}
+
+bool RunLoop::cancelToken(Token token) {
+    std::lock_guard<std::mutex> autoLock(mLock);
+
+    bool found = false;
+    for (auto it = mQueue.begin(); it != mQueue.end(); ++it) {
+        if (it->mToken == token) {
+            mQueue.erase(it);
+
+            if (it == mQueue.begin()) {
+                interrupt();
+            }
+
+            found = true;
+            break;
+        }
+    }
+
+    return found;
+}
+
+void RunLoop::postSocketRecv(int sock, AsyncFunction fn) {
+    CHECK_GE(sock, 0);
+    CHECK(fn != nullptr);
+
+    std::lock_guard<std::mutex> autoLock(mLock);
+    mAddInfos.push_back({ sock, InfoType::RECV, fn });
+    interrupt();
+}
+
+void RunLoop::postSocketSend(int sock, AsyncFunction fn) {
+    CHECK_GE(sock, 0);
+    CHECK(fn != nullptr);
+
+    std::lock_guard<std::mutex> autoLock(mLock);
+    mAddInfos.push_back({ sock, InfoType::SEND, fn });
+    interrupt();
+}
+
+void RunLoop::cancelSocket(int sock) {
+    CHECK_GE(sock, 0);
+
+    std::lock_guard<std::mutex> autoLock(mLock);
+    mAddInfos.push_back({ sock, InfoType::CANCEL, nullptr });
+    interrupt();
+}
+
+void RunLoop::insert(const QueueElem &elem) {
+    std::lock_guard<std::mutex> autoLock(mLock);
+
+    auto it = mQueue.begin();
+    while (it != mQueue.end() && *it <= elem) {
+        ++it;
+    }
+
+    if (it == mQueue.begin()) {
+        interrupt();
+    }
+
+    mQueue.insert(it, elem);
+}
+
+void RunLoop::run() {
+    mPThread = pthread_self();
+
+    std::map<int, SocketCallbacks> socketCallbacksByFd;
+    std::vector<pollfd> pollFds;
+
+    auto removePollFdAt = [&socketCallbacksByFd, &pollFds](size_t i) {
+        if (i + 1 == pollFds.size()) {
+            pollFds.pop_back();
+        } else {
+            // Instead of leaving a hole in the middle of the
+            // pollFds vector, we copy the last item into
+            // that hole and reduce the size of the vector by 1,
+            // taking are of updating the corresponding callback
+            // with the correct, new index.
+            pollFds[i] = pollFds.back();
+            pollFds.pop_back();
+            socketCallbacksByFd[pollFds[i].fd].mPollFdIndex = i;
+        }
+    };
+
+    // The control channel's pollFd will always be at index 0.
+    pollFds.push_back({ mControlFds[0], POLLIN, 0 });
+
+    for (;;) {
+        int timeoutMs = -1;  // wait Forever
+
+        {
+            std::lock_guard<std::mutex> autoLock(mLock);
+
+            if (mDone) {
+                break;
+            }
+
+            for (const auto &addInfo : mAddInfos) {
+                const int sock = addInfo.mSock;
+                const auto fn = addInfo.mFn;
+
+                auto it = socketCallbacksByFd.find(sock);
+
+                switch (addInfo.mType) {
+                    case InfoType::RECV:
+                    {
+                        if (it == socketCallbacksByFd.end()) {
+                            socketCallbacksByFd[sock] = { fn, nullptr, pollFds.size() };
+                            pollFds.push_back({ sock, POLLIN, 0 });
+                        } else {
+                            // There's already a pollFd for this socket.
+                            CHECK(it->second.mSendFn != nullptr);
+
+                            CHECK(it->second.mRecvFn == nullptr);
+                            it->second.mRecvFn = fn;
+
+                            pollFds[it->second.mPollFdIndex].events |= POLLIN;
+                        }
+                        break;
+                    }
+
+                    case InfoType::SEND:
+                    {
+                        if (it == socketCallbacksByFd.end()) {
+                            socketCallbacksByFd[sock] = { nullptr, fn, pollFds.size() };
+                            pollFds.push_back({ sock, POLLOUT, 0 });
+                        } else {
+                            // There's already a pollFd for this socket.
+                            if (it->second.mRecvFn == nullptr) {
+                                LOG(ERROR)
+                                    << "There's an entry but no recvFn "
+                                       "notification for socket "
+                                    << sock;
+                            }
+
+                            CHECK(it->second.mRecvFn != nullptr);
+
+                            if (it->second.mSendFn != nullptr) {
+                                LOG(ERROR)
+                                    << "There's already a pending send "
+                                       "notification for socket "
+                                    << sock;
+                            }
+                            CHECK(it->second.mSendFn == nullptr);
+                            it->second.mSendFn = fn;
+
+                            pollFds[it->second.mPollFdIndex].events |= POLLOUT;
+                        }
+                        break;
+                    }
+
+                    case InfoType::CANCEL:
+                    {
+                        if (it != socketCallbacksByFd.end()) {
+                            const size_t i = it->second.mPollFdIndex;
+
+                            socketCallbacksByFd.erase(it);
+                            removePollFdAt(i);
+                        }
+                        break;
+                    }
+                }
+            }
+
+            mAddInfos.clear();
+
+            if (!mQueue.empty()) {
+                timeoutMs = 0;
+
+                if (mQueue.front().mWhen) {
+                    auto duration =
+                        *mQueue.front().mWhen - std::chrono::steady_clock::now();
+
+                    auto durationMs =
+                        std::chrono::duration_cast<std::chrono::milliseconds>(duration);
+
+                    if (durationMs.count() > 0) {
+                        timeoutMs = static_cast<int>(durationMs.count());
+                    }
+                }
+            }
+        }
+
+        int pollRes = 0;
+        if (timeoutMs != 0) {
+            // NOTE: The inequality is on purpose, we'll want to execute this
+            // code if timeoutMs == -1 (infinite) or timeoutMs > 0, but not
+            // if it's 0.
+
+            pollRes = poll(
+                    pollFds.data(),
+                    static_cast<nfds_t>(pollFds.size()),
+                    timeoutMs);
+        }
+
+        if (pollRes < 0) {
+            if (errno != EINTR) {
+                std::cerr
+                    << "poll FAILED w/ "
+                    << errno
+                    << " ("
+                    << strerror(errno)
+                    << ")"
+                    << std::endl;
+            }
+
+            CHECK_EQ(errno, EINTR);
+            continue;
+        }
+
+        std::vector<AsyncFunction> fnArray;
+
+        {
+            std::lock_guard<std::mutex> autoLock(mLock);
+
+            if (pollRes > 0) {
+                if (pollFds[0].revents & POLLIN) {
+                    ssize_t res;
+                    do {
+                        uint8_t c[32];
+                        while ((res = read(mControlFds[0], c, sizeof(c))) < 0
+                                && errno == EINTR) {
+                        }
+                    } while (res > 0);
+                    CHECK(res < 0 && errno == EWOULDBLOCK);
+
+                    --pollRes;
+                }
+
+                // NOTE: Skip index 0, as we already handled it above.
+                // Also, bail early if we exhausted all actionable pollFds
+                // according to pollRes.
+                for (size_t i = pollFds.size(); pollRes && i-- > 1;) {
+                    pollfd &pollFd = pollFds[i];
+                    const short revents = pollFd.revents;
+
+                    if (revents) {
+                        --pollRes;
+                    }
+
+                    const bool readable = (revents & POLLIN);
+                    const bool writable = (revents & POLLOUT);
+                    const bool dead = (revents & POLLNVAL);
+
+                    bool removeCallback = dead;
+
+                    if (readable || writable || dead) {
+                        const int sock = pollFd.fd;
+
+                        const auto &it = socketCallbacksByFd.find(sock);
+                        auto &cb = it->second;
+                        CHECK_EQ(cb.mPollFdIndex, i);
+
+                        if (readable) {
+                            CHECK(cb.mRecvFn != nullptr);
+                            fnArray.push_back(cb.mRecvFn);
+                            cb.mRecvFn = nullptr;
+                            pollFd.events &= ~POLLIN;
+
+                            removeCallback |= (cb.mSendFn == nullptr);
+                        }
+
+                        if (writable) {
+                            CHECK(cb.mSendFn != nullptr);
+                            fnArray.push_back(cb.mSendFn);
+                            cb.mSendFn = nullptr;
+                            pollFd.events &= ~POLLOUT;
+
+                            removeCallback |= (cb.mRecvFn == nullptr);
+                        }
+
+                        if (removeCallback) {
+                            socketCallbacksByFd.erase(it);
+                            removePollFdAt(i);
+                        }
+                    }
+                }
+            } else {
+                // No interrupt, no socket notifications.
+                fnArray.push_back(mQueue.front().mFn);
+                mQueue.pop_front();
+            }
+        }
+
+        for (const auto &fn : fnArray) {
+            fn();
+        }
+    }
+}
+
+void RunLoop::interrupt() {
+    uint8_t c = 1;
+    ssize_t res;
+    while ((res = write(mControlFds[1], &c, 1)) < 0 && errno == EINTR) {
+    }
+
+    CHECK_EQ(res, 1);
+}
+
+struct MainRunLoop : public RunLoop {
+};
+
+static std::mutex gLock;
+static std::shared_ptr<RunLoop> gMainRunLoop;
+
+// static
+std::shared_ptr<RunLoop> RunLoop::main() {
+    std::lock_guard<std::mutex> autoLock(gLock);
+    if (!gMainRunLoop) {
+        gMainRunLoop = std::make_shared<MainRunLoop>();
+    }
+    return gMainRunLoop;
+}
+
+bool RunLoop::isCurrentThread() const {
+    return pthread_equal(pthread_self(), mPThread);
+}
+
diff --git a/host/frontend/gcastv2/https/SSLSocket.cpp b/host/frontend/gcastv2/https/SSLSocket.cpp
new file mode 100644
index 0000000..bcf2053
--- /dev/null
+++ b/host/frontend/gcastv2/https/SSLSocket.cpp
@@ -0,0 +1,653 @@
+#include <https/SSLSocket.h>
+
+#include <https/SafeCallbackable.h>
+#include <https/Support.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <sstream>
+#include <sys/socket.h>
+#include <utils/KeyStore.h>
+
+// static
+void SSLSocket::Init() {
+    SSL_library_init();
+    SSL_load_error_strings();
+}
+
+// static
+SSL_CTX *SSLSocket::CreateSSLContext() {
+    SSL_CTX *ctx = SSL_CTX_new(SSLv23_method());
+
+     /* Recommended to avoid SSLv2 & SSLv3 */
+     SSL_CTX_set_options(
+            ctx, SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
+
+    return ctx;
+}
+
+SSLSocket::SSLSocket(
+        std::shared_ptr<RunLoop> rl, Mode mode, int sock, uint32_t flags)
+    : BufferedSocket(rl, sock),
+      mMode(mode),
+      mFlags(flags),
+      mCtx(CreateSSLContext(), SSL_CTX_free),
+      mSSL(SSL_new(mCtx.get()), SSL_free),
+      mBioR(BIO_new(BIO_s_mem())),
+      mBioW(BIO_new(BIO_s_mem())),
+      mEOS(false),
+      mFinalErrno(0),
+      mRecvPending(false),
+      mRecvCallback(nullptr),
+      mSendPending(false),
+      mFlushFn(nullptr) {
+    if (mMode == Mode::ACCEPT) {
+        SSL_set_accept_state(mSSL.get());
+    } else {
+        SSL_set_connect_state(mSSL.get());
+    }
+    SSL_set_bio(mSSL.get(), mBioR, mBioW);
+}
+
+#ifdef TARGET_ANDROID_DEVICE
+
+static bool isResourcePath(const std::string &path, std::string *name) {
+    static const char *const kPrefix = "keystore:";
+    static const size_t kPrefixLen = strlen(kPrefix);
+
+    if (path.substr(0, kPrefixLen) != kPrefix) {
+        return false;
+    }
+
+    *name = path.substr(kPrefixLen);
+
+    return true;
+}
+
+#endif
+
+bool SSLSocket::useCertificate(const std::string &path) {
+#ifdef TARGET_ANDROID_DEVICE
+    std::string name;
+    if (isResourcePath(path, &name)) {
+        std::vector<uint8_t> data;
+        if (!getCertificateOrKey(name, &data)) {
+            return false;
+        }
+
+        std::unique_ptr<BIO, std::function<void(BIO *)>> cbio(
+                BIO_new_mem_buf(data.data(), data.size()), BIO_free);
+
+        X509 *cert = PEM_read_bio_X509(cbio.get(), nullptr, 0, nullptr);
+
+        return cert != nullptr && 1 == SSL_use_certificate(mSSL.get(), cert);
+    }
+#endif
+
+    return 1 == SSL_use_certificate_file(
+                mSSL.get(), path.c_str(), SSL_FILETYPE_PEM);
+}
+
+bool SSLSocket::usePrivateKey(const std::string &path) {
+#ifdef TARGET_ANDROID_DEVICE
+    std::string name;
+    if (isResourcePath(path, &name)) {
+        std::vector<uint8_t> data;
+        if (!getCertificateOrKey(name, &data)) {
+            return false;
+        }
+
+        std::unique_ptr<BIO, std::function<void(BIO *)>> cbio(
+                BIO_new_mem_buf(data.data(), data.size()), BIO_free);
+
+        RSA *key = PEM_read_bio_RSAPrivateKey(cbio.get(), nullptr, 0, nullptr);
+
+        return key != nullptr
+            && 1 == SSL_use_RSAPrivateKey(mSSL.get(), key)
+            && 1 == SSL_check_private_key(mSSL.get());
+    }
+#endif
+
+    return 1 == SSL_use_PrivateKey_file(
+            mSSL.get(), path.c_str(), SSL_FILETYPE_PEM)
+        && 1 == SSL_check_private_key(mSSL.get());
+}
+
+bool SSLSocket::useTrustedCertificates(const std::string &path) {
+#ifdef TARGET_ANDROID_DEVICE
+    std::string name;
+    if (isResourcePath(path, &name)) {
+        std::vector<uint8_t> data;
+        if (!getCertificateOrKey(name, &data)) {
+            return false;
+        }
+
+        std::unique_ptr<BIO, std::function<void(BIO *)>> cbio(
+                BIO_new_mem_buf(data.data(), data.size()), BIO_free);
+
+        // Shamelessly stolen from
+        // https://stackoverflow.com/questions/3810058/
+        //   read-certificate-files-from-memory-instead-of-a-file-using-openssl
+
+        X509_STORE *cts = SSL_CTX_get_cert_store(mCtx.get());
+
+        if (!cts) {
+            return false;
+        }
+
+        STACK_OF(X509_INFO) *info =
+            PEM_X509_INFO_read_bio(cbio.get(), nullptr, nullptr, nullptr);
+
+        if (!info) {
+            return false;
+        }
+
+        for (int i = 0; i < sk_X509_INFO_num(info); ++i) {
+            X509_INFO *tmp = sk_X509_INFO_value(info, i);
+
+            if (tmp->x509) {
+                X509_STORE_add_cert(cts, tmp->x509);
+            }
+
+            if (tmp->crl) {
+                X509_STORE_add_crl(cts, tmp->crl);
+            }
+        }
+
+        sk_X509_INFO_pop_free(info, X509_INFO_free);
+
+        return true;
+    }
+#endif
+
+    return 1 == SSL_CTX_load_verify_locations(
+            mCtx.get(),
+            path.c_str(),
+            nullptr /* CApath */);
+}
+
+SSLSocket::SSLSocket(
+        std::shared_ptr<RunLoop> rl,
+        int sock,
+        const std::string &certificate_pem_path,
+        const std::string &private_key_pem_path,
+        uint32_t flags)
+    : SSLSocket(rl, Mode::ACCEPT, sock, flags) {
+
+    // This flag makes no sense for a server.
+    CHECK(!(mFlags & FLAG_DONT_CHECK_PEER_CERTIFICATE));
+
+    CHECK(useCertificate(certificate_pem_path)
+            && usePrivateKey(private_key_pem_path));
+}
+
+SSLSocket::SSLSocket(
+        std::shared_ptr<RunLoop> rl,
+        int sock,
+        uint32_t flags,
+        const std::optional<std::string> &trusted_pem_path)
+    : SSLSocket(rl, Mode::CONNECT, sock, flags) {
+
+    if (!(mFlags & FLAG_DONT_CHECK_PEER_CERTIFICATE)) {
+        CHECK(trusted_pem_path.has_value());
+        CHECK(useTrustedCertificates(*trusted_pem_path));
+    }
+}
+
+SSLSocket::~SSLSocket() {
+    SSL_shutdown(mSSL.get());
+
+    mBioW = mBioR = nullptr;
+}
+
+void SSLSocket::postRecv(RunLoop::AsyncFunction fn) {
+    char tmp[128];
+    int n = SSL_peek(mSSL.get(), tmp, sizeof(tmp));
+
+    if (n > 0) {
+        fn();
+        return;
+    }
+
+    CHECK(mRecvCallback == nullptr);
+    mRecvCallback = fn;
+
+    if (!mRecvPending) {
+        mRecvPending = true;
+        runLoop()->postSocketRecv(
+                fd(),
+                makeSafeCallback(this, &SSLSocket::handleIncomingData));
+    }
+}
+
+void SSLSocket::handleIncomingData() {
+    mRecvPending = false;
+
+    uint8_t buffer[1024];
+    ssize_t len;
+    do {
+        len = ::recv(fd(), buffer, sizeof(buffer), 0);
+    } while (len < 0 && errno == EINTR);
+
+    if (len <= 0) {
+        mEOS = true;
+        mFinalErrno = (len < 0) ? errno : 0;
+
+        sendRecvCallback();
+        return;
+    }
+
+    size_t offset = 0;
+    while (len > 0) {
+        int n = BIO_write(mBioR, &buffer[offset], len);
+        CHECK_GT(n, 0);
+
+        offset += n;
+        len -= n;
+
+        if (!SSL_is_init_finished(mSSL.get())) {
+            if (mMode == Mode::ACCEPT) {
+                n = SSL_accept(mSSL.get());
+            } else {
+                n = SSL_connect(mSSL.get());
+            }
+
+            auto err = SSL_get_error(mSSL.get(), n);
+
+            switch (err) {
+                case SSL_ERROR_WANT_READ:
+                {
+                    CHECK_EQ(len, 0);
+                    queueOutputDataFromSSL();
+
+                    mRecvPending = true;
+
+                    runLoop()->postSocketRecv(
+                            fd(),
+                            makeSafeCallback(
+                                this, &SSLSocket::handleIncomingData));
+
+                    return;
+                }
+
+                case SSL_ERROR_WANT_WRITE:
+                {
+                    CHECK_EQ(len, 0);
+
+                    mRecvPending = true;
+
+                    runLoop()->postSocketRecv(
+                            fd(),
+                            makeSafeCallback(
+                                this, &SSLSocket::handleIncomingData));
+
+                    return;
+                }
+
+                case SSL_ERROR_NONE:
+                    break;
+
+                case SSL_ERROR_SYSCALL:
+                default:
+                {
+                    // This is where we end up if the client doesn't trust us.
+                    mEOS = true;
+                    mFinalErrno = ECONNREFUSED;
+
+                    sendRecvCallback();
+                    return;
+                }
+            }
+
+            CHECK(SSL_is_init_finished(mSSL.get()));
+
+            drainOutputBufferPlain();
+
+            if (!(mFlags & FLAG_DONT_CHECK_PEER_CERTIFICATE)
+                    && !isPeerCertificateValid()) {
+                mEOS = true;
+                mFinalErrno = ECONNREFUSED;
+                sendRecvCallback();
+            }
+        }
+    }
+
+    int n = SSL_peek(mSSL.get(), buffer, sizeof(buffer));
+
+    if (n > 0) {
+        sendRecvCallback();
+        return;
+    }
+
+    auto err = SSL_get_error(mSSL.get(), n);
+
+    switch (err) {
+        case SSL_ERROR_WANT_READ:
+        {
+            queueOutputDataFromSSL();
+
+            mRecvPending = true;
+
+            runLoop()->postSocketRecv(
+                    fd(),
+                    makeSafeCallback(this, &SSLSocket::handleIncomingData));
+
+            break;
+        }
+
+        case SSL_ERROR_WANT_WRITE:
+        {
+            mRecvPending = true;
+
+            runLoop()->postSocketRecv(
+                    fd(),
+                    makeSafeCallback(this, &SSLSocket::handleIncomingData));
+
+            break;
+        }
+
+        case SSL_ERROR_ZERO_RETURN:
+        {
+            mEOS = true;
+            mFinalErrno = 0;
+
+            sendRecvCallback();
+            break;
+        }
+
+        case SSL_ERROR_NONE:
+            break;
+
+        case SSL_ERROR_SYSCALL:
+        default:
+        {
+            // This is where we end up if the client doesn't trust us.
+            mEOS = true;
+            mFinalErrno = ECONNREFUSED;
+
+            sendRecvCallback();
+            break;
+        }
+    }
+}
+
+void SSLSocket::sendRecvCallback() {
+    const auto cb = mRecvCallback;
+    mRecvCallback = nullptr;
+    if (cb != nullptr) {
+        cb();
+    }
+}
+
+void SSLSocket::postSend(RunLoop::AsyncFunction fn) {
+    runLoop()->post(fn);
+}
+
+ssize_t SSLSocket::recvfrom(
+        void *data,
+        size_t size,
+        sockaddr *address,
+        socklen_t *addressLen) {
+    if (address || addressLen) {
+        errno = EINVAL;
+        return -1;
+    }
+
+    if (mEOS) {
+        errno = mFinalErrno;
+        return (mFinalErrno == 0) ? 0 : -1;
+    }
+
+    int n = SSL_read(mSSL.get(), data, size);
+
+    // We should only get here after SSL_peek signaled that there's data to
+    // be read.
+    CHECK_GT(n, 0);
+
+    return n;
+}
+
+void SSLSocket::queueOutputDataFromSSL() {
+    int n;
+    do {
+        char buf[1024];
+        n = BIO_read(mBioW, buf, sizeof(buf));
+
+        if (n > 0) {
+            queueOutputData(buf, n);
+        } else if (BIO_should_retry(mBioW)) {
+            continue;
+        } else {
+            TRESPASS();
+        }
+    } while (n > 0);
+}
+
+void SSLSocket::queueOutputData(const void *data, size_t size) {
+    if (!size) {
+        return;
+    }
+
+    const size_t pos = mOutBuffer.size();
+    mOutBuffer.resize(pos + size);
+    memcpy(mOutBuffer.data() + pos, data, size);
+
+    if (!mSendPending) {
+        mSendPending = true;
+        runLoop()->postSocketSend(
+                fd(),
+                makeSafeCallback(this, &SSLSocket::sendOutputData));
+    }
+}
+
+void SSLSocket::sendOutputData() {
+    mSendPending = false;
+
+    const size_t size = mOutBuffer.size();
+    size_t offset = 0;
+
+    while (offset < size) {
+        ssize_t n = ::send(
+                fd(), mOutBuffer.data() + offset, size - offset, 0);
+
+        if (n < 0) {
+            if (errno == EINTR) {
+                continue;
+            } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
+                break;
+            }
+
+            TRESPASS();
+        }
+
+        offset += static_cast<size_t>(n);
+    }
+
+    mOutBuffer.erase(mOutBuffer.begin(), mOutBuffer.begin() + offset);
+
+    if (!mOutBufferPlain.empty()) {
+        drainOutputBufferPlain();
+    }
+
+    if (!mOutBuffer.empty()) {
+        mSendPending = true;
+        runLoop()->postSocketSend(
+                fd(),
+                makeSafeCallback(this, &SSLSocket::sendOutputData));
+
+        return;
+    }
+
+    auto fn = mFlushFn;
+    mFlushFn = nullptr;
+    if (fn != nullptr) {
+        fn();
+    }
+}
+
+ssize_t SSLSocket::sendto(
+        const void *data,
+        size_t size,
+        const sockaddr *addr,
+        socklen_t addrLen) {
+    if (addr || addrLen) {
+        errno = -EINVAL;
+        return -1;
+    }
+
+    if (mEOS) {
+        errno = mFinalErrno;
+        return (mFinalErrno == 0) ? 0 : -1;
+    }
+
+    const size_t pos = mOutBufferPlain.size();
+    mOutBufferPlain.resize(pos + size);
+    memcpy(&mOutBufferPlain[pos], data, size);
+
+    drainOutputBufferPlain();
+
+    return size;
+}
+
+void SSLSocket::drainOutputBufferPlain() {
+    size_t offset = 0;
+    const size_t size = mOutBufferPlain.size();
+
+    while (offset < size) {
+        int n = SSL_write(mSSL.get(), &mOutBufferPlain[offset], size - offset);
+
+        if (!SSL_is_init_finished(mSSL.get())) {
+            if (mMode == Mode::ACCEPT) {
+                n = SSL_accept(mSSL.get());
+            } else {
+                n = SSL_connect(mSSL.get());
+            }
+
+            auto err = SSL_get_error(mSSL.get(), n);
+
+            switch (err) {
+                case SSL_ERROR_WANT_WRITE:
+                {
+                    mOutBufferPlain.erase(
+                            mOutBufferPlain.begin(),
+                            mOutBufferPlain.begin() + offset);
+
+                    queueOutputDataFromSSL();
+                    return;
+                }
+
+                case SSL_ERROR_WANT_READ:
+                {
+                    mOutBufferPlain.erase(
+                            mOutBufferPlain.begin(),
+                            mOutBufferPlain.begin() + offset);
+
+                    queueOutputDataFromSSL();
+
+                    if (!mRecvPending) {
+                        mRecvPending = true;
+
+                        runLoop()->postSocketRecv(
+                                fd(),
+                                makeSafeCallback(
+                                    this, &SSLSocket::handleIncomingData));
+                    }
+                    return;
+                }
+
+                case SSL_ERROR_SYSCALL:
+                {
+                    // This is where we end up if the client doesn't trust us.
+                    mEOS = true;
+                    mFinalErrno = ECONNREFUSED;
+
+                    TRESPASS();
+                    return;
+                }
+
+                case SSL_ERROR_NONE:
+                    break;
+
+                default:
+                    TRESPASS();
+            }
+
+            CHECK(SSL_is_init_finished(mSSL.get()));
+
+            if (!isPeerCertificateValid()) {
+                mEOS = true;
+                mFinalErrno = ECONNREFUSED;
+                sendRecvCallback();
+            }
+        }
+
+        offset += n;
+    }
+
+    mOutBufferPlain.erase(
+            mOutBufferPlain.begin(), mOutBufferPlain.begin() + offset);
+
+    queueOutputDataFromSSL();
+}
+
+bool SSLSocket::isPeerCertificateValid() {
+    if (mMode == Mode::ACCEPT || (mFlags & FLAG_DONT_CHECK_PEER_CERTIFICATE)) {
+        // For now we won't validate the client if we are the server.
+        return true;
+    }
+
+    std::unique_ptr<X509, std::function<void(X509 *)>> cert(
+            SSL_get_peer_certificate(mSSL.get()), X509_free);
+
+    if (!cert) {
+        LOG(ERROR) << "SSLSocket::isPeerCertificateValid no certificate.";
+
+        return false;
+    }
+
+    int res = SSL_get_verify_result(mSSL.get());
+
+    bool valid = (res == X509_V_OK);
+
+    if (!valid) {
+        LOG(ERROR) << "SSLSocket::isPeerCertificateValid invalid certificate.";
+
+        const EVP_MD *digest = EVP_get_digestbyname("sha256");
+
+        unsigned char md[EVP_MAX_MD_SIZE];
+        unsigned int n;
+        int res = X509_digest(cert.get(), digest, md, &n);
+        CHECK_EQ(res, 1);
+
+        std::stringstream ss;
+        for (unsigned int i = 0; i < n; ++i) {
+            if (i > 0) {
+                ss << ":";
+            }
+
+            auto byte = md[i];
+
+            auto nibble = byte >> 4;
+            ss << (char)((nibble < 10) ? ('0' + nibble) : ('A' + nibble - 10));
+
+            nibble = byte & 0x0f;
+            ss << (char)((nibble < 10) ? ('0' + nibble) : ('A' + nibble - 10));
+        }
+
+        LOG(ERROR)
+            << "Server offered certificate w/ fingerprint "
+            << ss.str();
+    }
+
+    return valid;
+}
+
+void SSLSocket::postFlush(RunLoop::AsyncFunction fn) {
+    CHECK(mFlushFn == nullptr);
+
+    if (!mSendPending) {
+        fn();
+        return;
+    }
+
+    mFlushFn = fn;
+}
+
diff --git a/host/frontend/gcastv2/https/ServerSocket.cpp b/host/frontend/gcastv2/https/ServerSocket.cpp
new file mode 100644
index 0000000..039e651
--- /dev/null
+++ b/host/frontend/gcastv2/https/ServerSocket.cpp
@@ -0,0 +1,166 @@
+#include <https/ServerSocket.h>
+
+#include <https/ClientSocket.h>
+#include <https/RunLoop.h>
+#include <https/SafeCallbackable.h>
+#include <https/Support.h>
+
+#include <media/stagefright/foundation/ADebug.h>
+
+#include <arpa/inet.h>
+#include <fcntl.h>
+#include <iostream>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+ServerSocket::ServerSocket(
+        HTTPServer *server,
+        TransportType transportType,
+        const char *iface,
+        uint16_t port,
+        const std::optional<std::string> &certificate_pem_path,
+        const std::optional<std::string> &private_key_pem_path)
+    : mInitCheck(-ENODEV),
+      mServer(server),
+      mCertificatePath(certificate_pem_path),
+      mPrivateKeyPath(private_key_pem_path),
+      mSocket(-1),
+      mTransportType(transportType) {
+    if (mTransportType == TransportType::TLS) {
+        CHECK(mCertificatePath.has_value());
+        CHECK(mPrivateKeyPath.has_value());
+    }
+
+    sockaddr_in addr;
+    int res;
+
+    mSocket = socket(PF_INET, SOCK_STREAM, 0);
+    if (mSocket < 0) {
+        mInitCheck = -errno;
+        goto bail;
+    }
+
+    makeFdNonblocking(mSocket);
+
+    static constexpr int yes = 1;
+    res = setsockopt(mSocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
+    if (res < 0) {
+        mInitCheck = -errno;
+        goto bail2;
+    }
+
+    memset(addr.sin_zero, 0, sizeof(addr.sin_zero));
+    addr.sin_family = AF_INET;
+    addr.sin_port = htons(port);
+    addr.sin_addr.s_addr = inet_addr(iface);
+
+    res = bind(mSocket, reinterpret_cast<sockaddr *>(&addr), sizeof(addr));
+    if (res < 0) {
+        mInitCheck = -errno;
+        goto bail2;
+    }
+
+    res = listen(mSocket, 4);
+    if (res < 0) {
+        mInitCheck = -errno;
+        goto bail2;
+    }
+
+    mInitCheck = 0;
+    return;
+
+bail2:
+    close(mSocket);
+    mSocket = -1;
+
+bail:
+    ;
+}
+
+ServerSocket::~ServerSocket() {
+    if (mSocket >= 0) {
+        close(mSocket);
+        mSocket = -1;
+    }
+}
+
+int ServerSocket::initCheck() const {
+    return mInitCheck;
+}
+
+ServerSocket::TransportType ServerSocket::transportType() const {
+    return mTransportType;
+}
+
+int ServerSocket::run(std::shared_ptr<RunLoop> rl) {
+    if (mInitCheck < 0) {
+        return mInitCheck;
+    }
+
+    if (mRunLoop) {
+        return -EBUSY;
+    }
+
+    mRunLoop = rl;
+    mRunLoop->postSocketRecv(
+            mSocket,
+            makeSafeCallback(this, &ServerSocket::acceptIncomingConnection));
+
+    return 0;
+}
+
+void ServerSocket::acceptIncomingConnection() {
+    sockaddr_in addr;
+    socklen_t addrLen = sizeof(addr);
+
+    int s = accept(mSocket, reinterpret_cast<sockaddr *>(&addr), &addrLen);
+
+    if (s >= 0) {
+        uint32_t ip = ntohl(addr.sin_addr.s_addr);
+
+        LOG(VERBOSE)
+            << "Accepted incoming connection from "
+            << (ip >> 24)
+            << "."
+            << ((ip >> 16) & 0xff)
+            << "."
+            << ((ip >> 8) & 0xff)
+            << "."
+            << (ip & 0xff)
+            << ":"
+            << ntohs(addr.sin_port);
+
+        makeFdNonblocking(s);
+
+        auto clientSocket =
+            std::make_shared<ClientSocket>(mRunLoop, mServer, this, addr, s);
+
+        clientSocket->run();
+
+        mClientSockets.push_back(clientSocket);
+    }
+
+    mRunLoop->postSocketRecv(
+            mSocket,
+            makeSafeCallback(this, &ServerSocket::acceptIncomingConnection));
+}
+
+void ServerSocket::onClientSocketClosed(int sock) {
+    for (size_t i = mClientSockets.size(); i--;) {
+        if (mClientSockets[i]->fd() == sock) {
+            LOG(VERBOSE) << "Closing client connection.";
+            mClientSockets.erase(mClientSockets.begin() + i);
+            break;
+        }
+    }
+}
+
+std::optional<std::string> ServerSocket::certificate_pem_path() const {
+    return mCertificatePath;
+}
+
+std::optional<std::string> ServerSocket::private_key_pem_path() const {
+    return mPrivateKeyPath;
+}
+
diff --git a/host/frontend/gcastv2/https/Support.cpp b/host/frontend/gcastv2/https/Support.cpp
new file mode 100644
index 0000000..72fd098
--- /dev/null
+++ b/host/frontend/gcastv2/https/Support.cpp
@@ -0,0 +1,50 @@
+#include <https/Support.h>
+
+#include <cassert>
+#include <cstdint>
+#include <cstdio>
+#include <ctype.h>
+#include <fcntl.h>
+#include <sys/errno.h>
+
+void makeFdNonblocking(int fd) {
+    int flags = fcntl(fd, F_GETFL, 0);
+    if (flags < 0) { flags = 0; }
+    DEBUG_ONLY(int res = )fcntl(fd, F_SETFL, flags | O_NONBLOCK);
+    assert(res >= 0);
+}
+
+void hexdump(const void *_data, size_t size) {
+  const uint8_t *data = static_cast<const uint8_t *>(_data);
+
+  size_t offset = 0;
+  while (offset < size) {
+    printf("%08zx: ", offset);
+
+    for (size_t col = 0; col < 16; ++col) {
+      if (offset + col < size) {
+        printf("%02x ", data[offset + col]);
+      } else {
+        printf("   ");
+      }
+
+      if (col == 7) {
+        printf(" ");
+      }
+    }
+
+    printf(" ");
+
+    for (size_t col = 0; col < 16; ++col) {
+      if (offset + col < size && isprint(data[offset + col])) {
+        printf("%c", data[offset + col]);
+      } else if (offset + col < size) {
+        printf(".");
+      }
+    }
+
+    printf("\n");
+
+    offset += 16;
+  }
+}
diff --git a/host/frontend/gcastv2/https/WebSocketHandler.cpp b/host/frontend/gcastv2/https/WebSocketHandler.cpp
new file mode 100644
index 0000000..8fe13f3
--- /dev/null
+++ b/host/frontend/gcastv2/https/WebSocketHandler.cpp
@@ -0,0 +1,206 @@
+#include <https/WebSocketHandler.h>
+
+#include <https/ClientSocket.h>
+#include <https/Support.h>
+
+#include <iostream>
+#include <media/stagefright/Utils.h>
+#include <sstream>
+
+ssize_t WebSocketHandler::handleRequest(
+        uint8_t *data, size_t size, bool isEOS) {
+    (void)isEOS;
+
+    size_t offset = 0;
+    while (offset + 1 < size) {
+        uint8_t *packet = &data[offset];
+        const size_t avail = size - offset;
+
+        size_t packetOffset = 0;
+        const uint8_t headerByte = packet[packetOffset];
+
+        const bool hasMask = (packet[packetOffset + 1] & 0x80) != 0;
+        size_t payloadLen = packet[packetOffset + 1] & 0x7f;
+        packetOffset += 2;
+
+        if (payloadLen == 126) {
+            if (packetOffset + 1 >= avail) {
+                break;
+            }
+
+            payloadLen = android::U16_AT(&packet[packetOffset]);
+            packetOffset += 2;
+        } else if (payloadLen == 127) {
+            if (packetOffset + 7 >= avail) {
+                break;
+            }
+
+            payloadLen = android::U64_AT(&packet[packetOffset]);
+            packetOffset += 8;
+        }
+
+        uint32_t mask = 0;
+        if (hasMask) {
+            if (packetOffset + 3 >= avail) {
+                break;
+            }
+
+            mask = android::U32_AT(&packet[packetOffset]);
+            packetOffset += 4;
+        }
+
+        if (packetOffset + payloadLen > avail) {
+            break;
+        }
+
+        if (mask) {
+            for (size_t i = 0; i < payloadLen; ++i) {
+                packet[packetOffset + i] ^= ((mask >> (8 * (3 - (i % 4)))) & 0xff);
+            }
+        }
+
+        int err = handleMessage(headerByte, &packet[packetOffset], payloadLen);
+
+        offset += packetOffset + payloadLen;
+
+        if (err < 0) {
+            return err;
+        }
+    }
+
+    return offset;
+}
+
+bool WebSocketHandler::isConnected() {
+    return mOutputCallback != nullptr || mClientSocket.lock() != nullptr;
+}
+
+void WebSocketHandler::setClientSocket(std::weak_ptr<ClientSocket> clientSocket) {
+    mClientSocket = clientSocket;
+}
+
+void WebSocketHandler::setOutputCallback(
+        const sockaddr_in &remoteAddr, OutputCallback fn) {
+    mOutputCallback = fn;
+    mRemoteAddr = remoteAddr;
+}
+
+int WebSocketHandler::handleMessage(
+        uint8_t headerByte, const uint8_t *msg, size_t len) {
+    std::cerr
+        << "WebSocketHandler::handleMessage(0x"
+        << std::hex
+        << (unsigned)headerByte
+        << std::dec
+        << ")"
+        << std::endl;
+
+    hexdump(msg, len);
+
+    const uint8_t opcode = headerByte & 0x0f;
+    if (opcode == 8) {
+        // Connection close.
+        return -1;
+    }
+
+    return 0;
+}
+
+int WebSocketHandler::sendMessage(
+        const void *data, size_t size, SendMode mode) {
+    static constexpr bool kUseMask = false;
+
+    size_t numHeaderBytes = 2 + (kUseMask ? 4 : 0);
+    if (size > 65535) {
+        numHeaderBytes += 8;
+    } else if (size > 125) {
+        numHeaderBytes += 2;
+    }
+
+    static constexpr uint8_t kOpCodeBySendMode[] = {
+        0x1,  // text
+        0x2,  // binary
+        0x8,  // closeConnection
+    };
+
+    auto opcode = kOpCodeBySendMode[static_cast<uint8_t>(mode)];
+
+    std::unique_ptr<uint8_t[]> buffer(new uint8_t[numHeaderBytes + size]);
+    uint8_t *msg = buffer.get();
+    msg[0] = 0x80 | opcode;  // FIN==1
+    msg[1] = kUseMask ? 0x80 : 0x00;
+
+    if (size > 65535) {
+        msg[1] |= 127;
+        msg[2] = 0x00;
+        msg[3] = 0x00;
+        msg[4] = 0x00;
+        msg[5] = 0x00;
+        msg[6] = (size >> 24) & 0xff;
+        msg[7] = (size >> 16) & 0xff;
+        msg[8] = (size >> 8) & 0xff;
+        msg[9] = size & 0xff;
+    } else if (size > 125) {
+        msg[1] |= 126;
+        msg[2] = (size >> 8) & 0xff;
+        msg[3] = size & 0xff;
+    } else {
+        msg[1] |= size;
+    }
+
+    if (kUseMask) {
+        uint32_t mask = rand();
+        msg[numHeaderBytes - 4] = (mask >> 24) & 0xff;
+        msg[numHeaderBytes - 3] = (mask >> 16) & 0xff;
+        msg[numHeaderBytes - 2] = (mask >> 8) & 0xff;
+        msg[numHeaderBytes - 1] = mask & 0xff;
+
+        for (size_t i = 0; i < size; ++i) {
+            msg[numHeaderBytes + i] =
+                ((const uint8_t *)data)[i]
+                    ^ ((mask >> (8 * (3 - (i % 4)))) & 0xff);
+        }
+    } else {
+        memcpy(&msg[numHeaderBytes], data, size);
+    }
+
+    if (mOutputCallback) {
+        mOutputCallback(msg, numHeaderBytes + size);
+    } else {
+        auto clientSocket = mClientSocket.lock();
+        if (clientSocket) {
+            clientSocket->queueOutputData(msg, numHeaderBytes + size);
+        }
+    }
+
+    return 0;
+}
+
+std::string WebSocketHandler::remoteHost() const {
+    sockaddr_in remoteAddr;
+
+    if (mOutputCallback) {
+        remoteAddr = mRemoteAddr;
+    } else {
+        auto clientSocket = mClientSocket.lock();
+        if (clientSocket) {
+            remoteAddr = clientSocket->remoteAddr();
+        } else {
+            return "0.0.0.0";
+        }
+    }
+
+    const uint32_t ipAddress = ntohl(remoteAddr.sin_addr.s_addr);
+
+    std::stringstream ss;
+    ss << (ipAddress >> 24)
+       << "."
+       << ((ipAddress >> 16) & 0xff)
+       << "."
+       << ((ipAddress >> 8) & 0xff)
+       << "."
+       << (ipAddress & 0xff);
+
+    return ss.str();
+}
+
diff --git a/host/frontend/gcastv2/https/certs/create_certs.sh b/host/frontend/gcastv2/https/certs/create_certs.sh
new file mode 100755
index 0000000..9f85e40
--- /dev/null
+++ b/host/frontend/gcastv2/https/certs/create_certs.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+# As explained in
+#  https://gist.github.com/darrenjs/4645f115d10aa4b5cebf57483ec82eca
+
+openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
+openssl rsa -passin pass:x -in server.pass.key -out server.key
+rm -f server.pass.key
+
+openssl req \
+    -subj "/C=US/ST=California/L=Santa Clara/O=Beyond Aggravated/CN=localhost" \
+    -new -key server.key -out server.csr
+
+openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt
+rm -f server.csr
+
+# Now create the list of certificates we trust as a client.
+
+rm trusted.pem
+
+# For now we just trust our own server.
+openssl x509 -in server.crt -text >> trusted.pem
+
+# Also add the system standard CA cert chain.
+# cat /opt/local/etc/openssl/cert.pem >> trusted.pem
+
+# Convert .pem to .der
+# openssl x509 -outform der -in trusted.pem -out trusted.der
+
+# Convert .crt and .key to .p12 for use by Security.framework
+# Enter password "foo"!
+openssl pkcs12 -export -inkey server.key -in server.crt -name localhost -out server.p12
diff --git a/host/frontend/gcastv2/https/certs/server.crt b/host/frontend/gcastv2/https/certs/server.crt
new file mode 100644
index 0000000..93108a7
--- /dev/null
+++ b/host/frontend/gcastv2/https/certs/server.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDVzCCAj8CFBlyltiL2iwsm/yiGCMKdiHNfnhkMA0GCSqGSIb3DQEBCwUAMGgx
+CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQHDAtTYW50
+YSBDbGFyYTEaMBgGA1UECgwRQmV5b25kIEFnZ3JhdmF0ZWQxEjAQBgNVBAMMCWxv
+Y2FsaG9zdDAeFw0xOTA4MjExNjQzNTdaFw0yMDA4MjAxNjQzNTdaMGgxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQHDAtTYW50YSBDbGFy
+YTEaMBgGA1UECgwRQmV5b25kIEFnZ3JhdmF0ZWQxEjAQBgNVBAMMCWxvY2FsaG9z
+dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANspCLPC//6LNZIArmxS
+YRrojSPiizASN0svdh1376Bgp9KjUw1NFQhMcq+IobGJjXlxvTqRiXaf3P0dJKZV
+Mdcyc9AOmCNwH8H86OmZTHwV3MdVVqPH2eYl+otWa2eLh8A0J7eN/xn9cl7xtG8u
+UqWc/0Qna7bp4kUOq70M4Kn1NGvlLhtT6jo6DJrpZTj64TTB2c420e86K6UBWSDW
+KPLmarV1mDrYo7gxSu1zqLssYit/X8Xdi8ESQ0WRe1QWNUxMk7qLFTQh1fSUGv6a
+pjWLLrGK9DeYdHsdXX2ttRAlDUtllGctypjCKc0Wcg4ZGfSXZoTlV2k8aEyjYFxu
+VbkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAwo6JLn09RZsTbMoMeCY6JXRMCff8
+mKN3gUao4ob8r6dmZjGDzhBK4oc04OZ28j/5VmnYnLgY0g/+MzbY7hCy35gee/rr
+6Z540m9vYtOrAKyquoOxrS43SciZMjpuL1PKT7Cvo7b6B26x81WSDLkms2NMltUZ
+V9Ayal61cIZOEsUd9JQesYrchoYqJjlqWhGcEe/Gy0O6rTt+ZaHHxS3RwCaQTDfw
+q3d2Z+WozMlOkFGvqhflI7uNuxiAFlwIrvWpn/k//f13x+jFfNLOUUPnzgFPxtWN
+v+yNlVX8KSG5mjgBrDKua5P8s+7zzmoprQk6+/SkUyieS6GgPO/aUF9lPg==
+-----END CERTIFICATE-----
diff --git a/host/frontend/gcastv2/https/certs/server.key b/host/frontend/gcastv2/https/certs/server.key
new file mode 100644
index 0000000..3c49d41
--- /dev/null
+++ b/host/frontend/gcastv2/https/certs/server.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA2ykIs8L//os1kgCubFJhGuiNI+KLMBI3Sy92HXfvoGCn0qNT
+DU0VCExyr4ihsYmNeXG9OpGJdp/c/R0kplUx1zJz0A6YI3Afwfzo6ZlMfBXcx1VW
+o8fZ5iX6i1ZrZ4uHwDQnt43/Gf1yXvG0by5SpZz/RCdrtuniRQ6rvQzgqfU0a+Uu
+G1PqOjoMmullOPrhNMHZzjbR7zorpQFZINYo8uZqtXWYOtijuDFK7XOouyxiK39f
+xd2LwRJDRZF7VBY1TEyTuosVNCHV9JQa/pqmNYsusYr0N5h0ex1dfa21ECUNS2WU
+Zy3KmMIpzRZyDhkZ9JdmhOVXaTxoTKNgXG5VuQIDAQABAoIBADSgKNfJlmEQGoAB
+q+CU835bozCfwJ59cUubjPhMiXzwWLwqvdGu1cMVYGCoJXOVPSSRXEgJFIIopw8c
+hWVvhbVQByFJr9yqmuWhDmixh/0v3sD6zAAuCDPMoNn+mXHWvnU30OWxsjUSd/k2
+fwZHkE72YRGDicj1ZoEAodqWkggdSIDSxenT7U7SdoIMzEX7s68g9rt338zjgHdx
+r8qYvYqSB6qufNk8vQ/Rq5cz6gBNCrqrJkoC+pzZY0Ps61fEOX7HiyjAxmUSHViq
+6d1+BcAhtoXCy47WmuUH5XD0JixaYtrwon+LTxS/57X9Te+iKFCeLeTkEL++w9TW
+U92DEQECgYEA8MrKzdH79i4dduIIVecwvOE1EPb3Clz89bihFK3aRMPQn1sRMoLB
+glOLnygAn6m3FvIBQYOLwu77+Hfu20Wn0AHHboZ6TOzOLw14bIegA8JUpWmGFKp8
+QGRLBCbFDe/Jzldbmnz5+czmfGC976ZnrAcltchliLqIY51Tlcy7iWECgYEA6QB9
+lDfwpzRHkv9Higcr5ek/jJh6yxa/lolJ8da18HXz8CNsESIUQzQ5NafiH0UKmHNb
+lU0d3XNUWbciauIkZBp/hI7a3qpBvBpMIibhVbbYZwffHIcB2sl9aMNvbRepGSin
+8RhS1/gjYGpDZUFAb9nI+Q9ds1LhtDuVNYWlc1kCgYBnFstA1vfCa/fdX+Qlsvcg
+xmJLww/89dau9L6x9cSO0C7PKtPwBh8GxvHtz9iEgsqKHk3/WGxLprllpSMz6Vxx
+J3HUlepiaLfVWTUHcgFF2rsWnljzHBJaFxYuztJqpxCFMM1dFMiI8/pxoZFvupSE
+CANTElq18PZXf5G7fo2DwQKBgEmEFBteAxpejm69CQ9t79Xc4GY1/hhyFwUiUChz
+/HfgX5VXN9O4EviZU6uwStJe8FblnvHeoRusNeMQu1VKAP1stutWP4yd35vAGGF2
+LwbiaCkp/KV9m4IyrNUPROsA2iPMBiE18X2fF1pmbNlX20LvTk/3HzNjZT9+xTdb
+8bYRAoGBANKwX+QCpCQCNf+hxTPUjf4reShz5w/Wpk2xPxa+FnVxe3jtyxAOZ5uW
+lUWe7UXyNSWh8oiH8japwMkgN+dGnrzW5rMiXwpBFROAqgblRdbvLafebyGQBwAV
+2l6G4RfgG7LJAWsHu0rA74ZwOknfXfebxtrcLhfU7UYdl7fus71D
+-----END RSA PRIVATE KEY-----
diff --git a/host/frontend/gcastv2/https/certs/trusted.pem b/host/frontend/gcastv2/https/certs/trusted.pem
new file mode 100644
index 0000000..3429b75
--- /dev/null
+++ b/host/frontend/gcastv2/https/certs/trusted.pem
@@ -0,0 +1,70 @@
+Certificate:
+    Data:
+        Version: 1 (0x0)
+        Serial Number:
+            19:72:96:d8:8b:da:2c:2c:9b:fc:a2:18:23:0a:76:21:cd:7e:78:64
+        Signature Algorithm: sha256WithRSAEncryption
+        Issuer: C = US, ST = California, L = Santa Clara, O = Beyond Aggravated, CN = localhost
+        Validity
+            Not Before: Aug 21 16:43:57 2019 GMT
+            Not After : Aug 20 16:43:57 2020 GMT
+        Subject: C = US, ST = California, L = Santa Clara, O = Beyond Aggravated, CN = localhost
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                RSA Public-Key: (2048 bit)
+                Modulus:
+                    00:db:29:08:b3:c2:ff:fe:8b:35:92:00:ae:6c:52:
+                    61:1a:e8:8d:23:e2:8b:30:12:37:4b:2f:76:1d:77:
+                    ef:a0:60:a7:d2:a3:53:0d:4d:15:08:4c:72:af:88:
+                    a1:b1:89:8d:79:71:bd:3a:91:89:76:9f:dc:fd:1d:
+                    24:a6:55:31:d7:32:73:d0:0e:98:23:70:1f:c1:fc:
+                    e8:e9:99:4c:7c:15:dc:c7:55:56:a3:c7:d9:e6:25:
+                    fa:8b:56:6b:67:8b:87:c0:34:27:b7:8d:ff:19:fd:
+                    72:5e:f1:b4:6f:2e:52:a5:9c:ff:44:27:6b:b6:e9:
+                    e2:45:0e:ab:bd:0c:e0:a9:f5:34:6b:e5:2e:1b:53:
+                    ea:3a:3a:0c:9a:e9:65:38:fa:e1:34:c1:d9:ce:36:
+                    d1:ef:3a:2b:a5:01:59:20:d6:28:f2:e6:6a:b5:75:
+                    98:3a:d8:a3:b8:31:4a:ed:73:a8:bb:2c:62:2b:7f:
+                    5f:c5:dd:8b:c1:12:43:45:91:7b:54:16:35:4c:4c:
+                    93:ba:8b:15:34:21:d5:f4:94:1a:fe:9a:a6:35:8b:
+                    2e:b1:8a:f4:37:98:74:7b:1d:5d:7d:ad:b5:10:25:
+                    0d:4b:65:94:67:2d:ca:98:c2:29:cd:16:72:0e:19:
+                    19:f4:97:66:84:e5:57:69:3c:68:4c:a3:60:5c:6e:
+                    55:b9
+                Exponent: 65537 (0x10001)
+    Signature Algorithm: sha256WithRSAEncryption
+         c2:8e:89:2e:7d:3d:45:9b:13:6c:ca:0c:78:26:3a:25:74:4c:
+         09:f7:fc:98:a3:77:81:46:a8:e2:86:fc:af:a7:66:66:31:83:
+         ce:10:4a:e2:87:34:e0:e6:76:f2:3f:f9:56:69:d8:9c:b8:18:
+         d2:0f:fe:33:36:d8:ee:10:b2:df:98:1e:7b:fa:eb:e9:9e:78:
+         d2:6f:6f:62:d3:ab:00:ac:aa:ba:83:b1:ad:2e:37:49:c8:99:
+         32:3a:6e:2f:53:ca:4f:b0:af:a3:b6:fa:07:6e:b1:f3:55:92:
+         0c:b9:26:b3:63:4c:96:d5:19:57:d0:32:6a:5e:b5:70:86:4e:
+         12:c5:1d:f4:94:1e:b1:8a:dc:86:86:2a:26:39:6a:5a:11:9c:
+         11:ef:c6:cb:43:ba:ad:3b:7e:65:a1:c7:c5:2d:d1:c0:26:90:
+         4c:37:f0:ab:77:76:67:e5:a8:cc:c9:4e:90:51:af:aa:17:e5:
+         23:bb:8d:bb:18:80:16:5c:08:ae:f5:a9:9f:f9:3f:fd:fd:77:
+         c7:e8:c5:7c:d2:ce:51:43:e7:ce:01:4f:c6:d5:8d:bf:ec:8d:
+         95:55:fc:29:21:b9:9a:38:01:ac:32:ae:6b:93:fc:b3:ee:f3:
+         ce:6a:29:ad:09:3a:fb:f4:a4:53:28:9e:4b:a1:a0:3c:ef:da:
+         50:5f:65:3e
+-----BEGIN CERTIFICATE-----
+MIIDVzCCAj8CFBlyltiL2iwsm/yiGCMKdiHNfnhkMA0GCSqGSIb3DQEBCwUAMGgx
+CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQHDAtTYW50
+YSBDbGFyYTEaMBgGA1UECgwRQmV5b25kIEFnZ3JhdmF0ZWQxEjAQBgNVBAMMCWxv
+Y2FsaG9zdDAeFw0xOTA4MjExNjQzNTdaFw0yMDA4MjAxNjQzNTdaMGgxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQHDAtTYW50YSBDbGFy
+YTEaMBgGA1UECgwRQmV5b25kIEFnZ3JhdmF0ZWQxEjAQBgNVBAMMCWxvY2FsaG9z
+dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANspCLPC//6LNZIArmxS
+YRrojSPiizASN0svdh1376Bgp9KjUw1NFQhMcq+IobGJjXlxvTqRiXaf3P0dJKZV
+Mdcyc9AOmCNwH8H86OmZTHwV3MdVVqPH2eYl+otWa2eLh8A0J7eN/xn9cl7xtG8u
+UqWc/0Qna7bp4kUOq70M4Kn1NGvlLhtT6jo6DJrpZTj64TTB2c420e86K6UBWSDW
+KPLmarV1mDrYo7gxSu1zqLssYit/X8Xdi8ESQ0WRe1QWNUxMk7qLFTQh1fSUGv6a
+pjWLLrGK9DeYdHsdXX2ttRAlDUtllGctypjCKc0Wcg4ZGfSXZoTlV2k8aEyjYFxu
+VbkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAwo6JLn09RZsTbMoMeCY6JXRMCff8
+mKN3gUao4ob8r6dmZjGDzhBK4oc04OZ28j/5VmnYnLgY0g/+MzbY7hCy35gee/rr
+6Z540m9vYtOrAKyquoOxrS43SciZMjpuL1PKT7Cvo7b6B26x81WSDLkms2NMltUZ
+V9Ayal61cIZOEsUd9JQesYrchoYqJjlqWhGcEe/Gy0O6rTt+ZaHHxS3RwCaQTDfw
+q3d2Z+WozMlOkFGvqhflI7uNuxiAFlwIrvWpn/k//f13x+jFfNLOUUPnzgFPxtWN
+v+yNlVX8KSG5mjgBrDKua5P8s+7zzmoprQk6+/SkUyieS6GgPO/aUF9lPg==
+-----END CERTIFICATE-----
diff --git a/host/frontend/gcastv2/https/content/index.html b/host/frontend/gcastv2/https/content/index.html
new file mode 100644
index 0000000..081f46a
--- /dev/null
+++ b/host/frontend/gcastv2/https/content/index.html
@@ -0,0 +1,19 @@
+<script>
+
+var sock = new WebSocket("wss://localhost:8443/control", "protocolOne");
+
+sock.onopen = function(event) {
+    // Close connection after 5 secs.
+    // setTimeout(function() { sock.close(); }, 5000);
+
+    sock.send("Here's some sample text");
+}
+
+sock.onmessage = function(event) {
+    console.log(event.data);
+}
+
+</script>
+
+<h1>Test</h1>
+
diff --git a/host/frontend/gcastv2/https/include/https/BaseConnection.h b/host/frontend/gcastv2/https/include/https/BaseConnection.h
new file mode 100644
index 0000000..eb96155
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/BaseConnection.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <https/BufferedSocket.h>
+#include <https/RunLoop.h>
+
+#include <memory>
+#include <vector>
+
+struct BaseConnection : public std::enable_shared_from_this<BaseConnection> {
+    explicit BaseConnection(std::shared_ptr<RunLoop> runLoop, int sock);
+    virtual ~BaseConnection() = default;
+
+    void run();
+
+    BaseConnection(const BaseConnection &) = delete;
+    BaseConnection &operator=(const BaseConnection &) = delete;
+
+protected:
+    // Return -EAGAIN to indicate that not enough data was provided (yet).
+    // Return a positive (> 0) value to drain some amount of data.
+    // Return values <= 0 are considered an error.
+    virtual ssize_t processClientRequest(const void *data, size_t size) = 0;
+
+    virtual void onDisconnect(int err) = 0;
+
+    void send(const void *_data, size_t size);
+
+    int fd() const;
+
+private:
+    std::shared_ptr<RunLoop> mRunLoop;
+
+    std::unique_ptr<BufferedSocket> mSocket;
+
+    std::vector<uint8_t> mInBuffer;
+    size_t mInBufferLen;
+
+    bool mSendPending;
+    std::vector<uint8_t> mOutBuffer;
+
+    void receiveClientRequest();
+    void sendOutputData();
+
+    void onClientRequest();
+};
diff --git a/host/frontend/gcastv2/https/include/https/BufferedSocket.h b/host/frontend/gcastv2/https/include/https/BufferedSocket.h
new file mode 100644
index 0000000..1bce0c4
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/BufferedSocket.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <https/RunLoop.h>
+
+#include <sys/socket.h>
+
+struct BufferedSocket {
+    explicit BufferedSocket(std::shared_ptr<RunLoop> rl, int sock);
+    virtual ~BufferedSocket();
+
+    BufferedSocket(const BufferedSocket &) = delete;
+    BufferedSocket &operator=(const BufferedSocket &) = delete;
+
+    virtual void postRecv(RunLoop::AsyncFunction fn) = 0;
+    virtual void postSend(RunLoop::AsyncFunction fn) = 0;
+
+    ssize_t recv(void *data, size_t size) {
+        return recvfrom(data, size, nullptr, nullptr);
+    }
+
+    virtual ssize_t recvfrom(
+            void *data,
+            size_t size,
+            sockaddr *address,
+            socklen_t *addressLen) = 0;
+
+    ssize_t send(const void *data, size_t size) {
+        return sendto(data, size, nullptr, 0);
+    }
+
+    virtual ssize_t sendto(
+            const void *data,
+            size_t size,
+            const sockaddr *addr,
+            socklen_t addrLen) = 0;
+
+    virtual void postFlush(RunLoop::AsyncFunction fn) = 0;
+
+    int fd() const;
+
+protected:
+    RunLoop *runLoop();
+
+private:
+    std::shared_ptr<RunLoop> mRunLoop;
+    int mSock;
+};
diff --git a/host/frontend/gcastv2/https/include/https/ClientSocket.h b/host/frontend/gcastv2/https/include/https/ClientSocket.h
new file mode 100644
index 0000000..b7847b2
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/ClientSocket.h
@@ -0,0 +1,70 @@
+#include <https/BufferedSocket.h>
+
+#include <https/WebSocketHandler.h>
+
+#include <arpa/inet.h>
+#include <vector>
+#include <memory>
+
+#include <https/PlainSocket.h>
+#include <https/SSLSocket.h>
+
+struct HTTPServer;
+struct RunLoop;
+struct ServerSocket;
+
+struct ClientSocket : public std::enable_shared_from_this<ClientSocket> {
+    explicit ClientSocket(
+            std::shared_ptr<RunLoop> rl,
+            HTTPServer *server,
+            ServerSocket *parent,
+            const sockaddr_in &addr,
+            int sock);
+
+    ClientSocket(const ClientSocket &) = delete;
+    ClientSocket &operator=(const ClientSocket &) = delete;
+
+    void run();
+
+    int fd() const;
+
+    void queueResponse(const std::string &response, const std::string &body);
+    void setWebSocketHandler(std::shared_ptr<WebSocketHandler> handler);
+
+    void queueOutputData(const uint8_t *data, size_t size);
+
+    sockaddr_in remoteAddr() const;
+
+private:
+    std::shared_ptr<RunLoop> mRunLoop;
+    HTTPServer *mServer;
+    ServerSocket *mParent;
+    sockaddr_in mRemoteAddr;
+
+    std::shared_ptr<BufferedSocket> mImplPlain;
+    std::shared_ptr<SSLSocket> mImplSSL;
+
+    std::vector<uint8_t> mInBuffer;
+    size_t mInBufferLen;
+
+    std::vector<uint8_t> mOutBuffer;
+    bool mSendPending;
+
+    bool mDisconnecting;
+
+    std::shared_ptr<WebSocketHandler> mWebSocketHandler;
+
+    void handleIncomingData();
+
+    // Returns true iff the client should close the connection.
+    bool handleRequest(bool isEOS);
+
+    void sendOutputData();
+
+    void disconnect();
+    void finishDisconnect();
+
+    BufferedSocket *getImpl() const {
+        return mImplSSL ? mImplSSL.get() : mImplPlain.get();
+    }
+};
diff --git a/host/frontend/gcastv2/https/include/https/HTTPClientConnection.h b/host/frontend/gcastv2/https/include/https/HTTPClientConnection.h
new file mode 100644
index 0000000..16c73bc
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/HTTPClientConnection.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include <arpa/inet.h>
+#include <https/BufferedSocket.h>
+#include <https/ServerSocket.h>
+#include <https/WebSocketHandler.h>
+
+#include <memory>
+#include <optional>
+#include <string>
+
+struct HTTPClientConnection
+    : public std::enable_shared_from_this<HTTPClientConnection> {
+
+    explicit HTTPClientConnection(
+            std::shared_ptr<RunLoop> rl,
+            std::shared_ptr<WebSocketHandler> webSocketHandler,
+            std::string_view path,
+            ServerSocket::TransportType transportType,
+            const std::optional<std::string> &trusted_pem_path =  std::nullopt);
+
+    HTTPClientConnection(const HTTPClientConnection &) = delete;
+    HTTPClientConnection &operator=(const HTTPClientConnection &) = delete;
+
+    int initCheck() const;
+
+    int connect(const char *host, uint16_t port);
+
+private:
+    int mInitCheck;
+
+    std::shared_ptr<RunLoop> mRunLoop;
+    std::shared_ptr<WebSocketHandler> mWebSocketHandler;
+    std::string mPath;
+    ServerSocket::TransportType mTransportType;
+    std::shared_ptr<BufferedSocket> mImpl;
+
+    std::vector<uint8_t> mOutBuffer;
+    bool mSendPending;
+
+    std::vector<uint8_t> mInBuffer;
+    size_t mInBufferLen;
+
+    sockaddr_in mRemoteAddr;
+
+    bool mWebSocketMode;
+
+    void sendRequest();
+    void receiveResponse();
+
+    // Returns true iff response was received fully.
+    bool handleResponse(bool isEOS);
+
+    void queueOutputData(const uint8_t *data, size_t size);
+    void sendOutputData();
+};
+
diff --git a/host/frontend/gcastv2/https/include/https/HTTPRequestResponse.h b/host/frontend/gcastv2/https/include/https/HTTPRequestResponse.h
new file mode 100644
index 0000000..db89ba2
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/HTTPRequestResponse.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include <cstdint>
+#include <map>
+#include <string>
+#include <string_view>
+
+struct HTTPRequestResponse {
+    explicit HTTPRequestResponse();
+    virtual ~HTTPRequestResponse() = default;
+
+    HTTPRequestResponse(const HTTPRequestResponse &) = delete;
+    HTTPRequestResponse &operator=(const HTTPRequestResponse &) = delete;
+
+    int setTo(const uint8_t *data, size_t size);
+    int initCheck() const;
+
+    bool getHeaderField(std::string_view key, std::string *value) const;
+
+    size_t getContentLength() const;
+
+protected:
+    virtual bool parseRequestResponseLine(const std::string &line) = 0;
+
+private:
+    struct CaseInsensitiveCompare {
+        bool operator()(const std::string &a, const std::string &b) const {
+            return strcasecmp(a.c_str(), b.c_str()) < 0;
+        }
+    };
+
+    int mInitCheck;
+
+    size_t mContentLength;
+
+    std::map<std::string, std::string, CaseInsensitiveCompare> mHeaders;
+};
+
+struct HTTPRequest : public HTTPRequestResponse {
+    std::string getMethod() const;
+    std::string getPath() const;
+    std::string getVersion() const;
+
+protected:
+    bool parseRequestResponseLine(const std::string &line) override;
+
+private:
+    std::string mMethod;
+    std::string mPath;
+    std::string mVersion;
+};
+
+struct HTTPResponse : public HTTPRequestResponse {
+    std::string getVersion() const;
+    int32_t getStatusCode() const;
+    std::string getStatusMessage() const;
+
+protected:
+    bool parseRequestResponseLine(const std::string &line) override;
+
+private:
+    std::string mVersion;
+    std::string mStatusMessage;
+    int32_t mStatusCode;
+};
+
diff --git a/host/frontend/gcastv2/https/include/https/HTTPServer.h b/host/frontend/gcastv2/https/include/https/HTTPServer.h
new file mode 100644
index 0000000..07996ae
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/HTTPServer.h
@@ -0,0 +1,94 @@
+#pragma once
+
+#include <https/ServerSocket.h>
+#include <https/WebSocketHandler.h>
+
+#include <functional>
+#include <mutex>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <variant>
+#include <vector>
+
+struct HTTPRequest;
+
+struct HTTPServer {
+    explicit HTTPServer(
+            std::shared_ptr<RunLoop> runLoop,
+            const char *iface = nullptr, // defaults to 0.0.0.0, i.e. INADDR_ANY
+            uint16_t port = 8443,
+            ServerSocket::TransportType transportType =
+                    ServerSocket::TransportType::TLS,
+            const std::optional<std::string> &certificate_pem_path =
+                    std::nullopt,
+            const std::optional<std::string> &private_key_pem_path =
+                    std::nullopt);
+
+    HTTPServer(const HTTPServer &) = delete;
+    HTTPServer &operator=(const HTTPServer &) = delete;
+
+    uint16_t getLocalPort() const;
+
+    void run();
+
+    // Returns true iff the client should close the connection.
+    bool handleSingleRequest(
+            ClientSocket *client, const uint8_t *data, size_t size, bool isEOS);
+
+    void addStaticFile(
+            const char *at,
+            const char *path,
+            std::optional<std::string> mimeType = std::nullopt);
+
+    void addStaticContent(
+            const char *at,
+            const void *data,
+            size_t size,
+            std::optional<std::string> mimeType = std::nullopt);
+
+    using WebSocketHandlerFactory =
+        std::function<std::pair<int32_t, std::shared_ptr<WebSocketHandler>>()>;
+
+    void addWebSocketHandlerFactory(
+            const char *at, WebSocketHandlerFactory factory);
+
+    std::optional<std::string> certificate_pem_path() const;
+    std::optional<std::string> private_key_pem_path() const;
+
+private:
+    struct StaticFileInfo {
+        std::variant<std::string, std::vector<uint8_t>> mPathOrContent;
+        std::optional<std::string> mMimeType;
+    };
+
+    std::shared_ptr<RunLoop> mRunLoop;
+    uint16_t mLocalPort;
+
+    std::shared_ptr<ServerSocket> mSocketTLS;
+
+    // Protects mStaticFiles and mWebSocketHandlerFactories below.
+    std::mutex mContentLock;
+
+    std::unordered_map<std::string, StaticFileInfo> mStaticFiles;
+
+    std::unordered_map<std::string, WebSocketHandlerFactory>
+        mWebSocketHandlerFactories;
+
+    void handleWebSocketRequest(
+            ClientSocket *clientSocket,
+            WebSocketHandlerFactory factory,
+            const HTTPRequest &request,
+            int32_t *httpResultCode,
+            std::unordered_map<std::string, std::string> *responseHeaders,
+            std::string *body);
+
+    void handleStaticFileRequest(
+            const StaticFileInfo &info,
+            const HTTPRequest &request,
+            int32_t *httpResultCode,
+            std::unordered_map<std::string, std::string> *responseHeaders,
+            std::string *body);
+
+    static std::string GuessMimeType(const std::string &path);
+};
diff --git a/host/frontend/gcastv2/https/include/https/PlainSocket.h b/host/frontend/gcastv2/https/include/https/PlainSocket.h
new file mode 100644
index 0000000..f58649c
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/PlainSocket.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <https/BufferedSocket.h>
+
+struct PlainSocket : public BufferedSocket {
+    explicit PlainSocket(std::shared_ptr<RunLoop> rl, int sock);
+
+    PlainSocket(const PlainSocket &) = delete;
+    PlainSocket &operator=(const PlainSocket &) = delete;
+
+    void postRecv(RunLoop::AsyncFunction fn) override;
+    void postSend(RunLoop::AsyncFunction fn) override;
+
+    ssize_t recvfrom(
+            void *data,
+            size_t size,
+            sockaddr *address,
+            socklen_t *addressLen) override;
+
+    ssize_t sendto(
+            const void *data,
+            size_t size,
+            const sockaddr *addr,
+            socklen_t addrLen) override;
+
+    void postFlush(RunLoop::AsyncFunction fn) override;
+};
diff --git a/host/frontend/gcastv2/https/include/https/RunLoop.h b/host/frontend/gcastv2/https/include/https/RunLoop.h
new file mode 100644
index 0000000..c189a20
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/RunLoop.h
@@ -0,0 +1,100 @@
+#pragma once
+
+#include <atomic>
+#include <chrono>
+#include <deque>
+#include <functional>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <poll.h>
+#include <string>
+#include <string_view>
+#include <thread>
+#include <vector>
+
+#include <sys/select.h>
+
+struct MainRunLoop;
+
+struct RunLoop {
+    explicit RunLoop(std::string_view name);
+    ~RunLoop();
+
+    static std::shared_ptr<RunLoop> main();
+
+    // For public use on the main RunLoop only.
+    void stop();
+    void run();
+
+    RunLoop(const RunLoop &) = delete;
+    RunLoop &operator=(const RunLoop &) = delete;
+
+    typedef std::function<void()> AsyncFunction;
+    typedef int32_t Token;
+
+    Token post(AsyncFunction fn);
+
+    Token postWithDelay(
+            std::chrono::steady_clock::duration delay, AsyncFunction fn);
+
+    // Returns true iff matching event was cancelled.
+    bool cancelToken(Token token);
+
+    void postSocketRecv(int sock, AsyncFunction fn);
+    void postSocketSend(int sock, AsyncFunction fn);
+    void cancelSocket(int sock);
+
+    bool isCurrentThread() const;
+
+private:
+    friend struct MainRunLoop;
+
+    struct QueueElem {
+        std::optional<std::chrono::steady_clock::time_point> mWhen;
+        AsyncFunction mFn;
+        Token mToken;
+
+        bool operator<=(const QueueElem &other) const;
+    };
+
+    struct SocketCallbacks {
+        AsyncFunction mRecvFn;
+        AsyncFunction mSendFn;
+        size_t mPollFdIndex;
+    };
+
+    enum class InfoType {
+        RECV,
+        SEND,
+        CANCEL,
+    };
+
+    struct AddSocketCallbackInfo {
+        int mSock;
+        InfoType mType;
+        AsyncFunction mFn;
+    };
+
+    std::string mName;
+
+    int mControlFds[2];
+
+    std::mutex mLock;
+    std::deque<QueueElem> mQueue;
+    std::vector<AddSocketCallbackInfo> mAddInfos;
+
+    std::atomic<bool> mDone;
+    std::thread mThread;
+    pthread_t mPThread;
+
+    std::atomic<Token> mNextToken;
+
+    explicit RunLoop();  // constructor for the main RunLoop.
+
+    void interrupt();
+    void insert(const QueueElem &elem);
+    void addPollFd_l(int sock);
+};
+
diff --git a/host/frontend/gcastv2/https/include/https/SSLSocket.h b/host/frontend/gcastv2/https/include/https/SSLSocket.h
new file mode 100644
index 0000000..9e2ce57
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/SSLSocket.h
@@ -0,0 +1,104 @@
+#include <https/BufferedSocket.h>
+
+#include <openssl/bio.h>
+#include <openssl/err.h>
+#include <openssl/pem.h>
+#include <openssl/ssl.h>
+
+#include <memory>
+
+struct SSLSocket
+    : public BufferedSocket,
+      public std::enable_shared_from_this<SSLSocket> {
+    static void Init();
+
+    enum {
+        // Client only.
+        FLAG_DONT_CHECK_PEER_CERTIFICATE = 1,
+    };
+
+    explicit SSLSocket(
+            std::shared_ptr<RunLoop> rl,
+            int sock,
+            const std::string &certificate_pem_path,
+            const std::string &private_key_pem_path,
+            uint32_t flags = 0);
+
+    explicit SSLSocket(
+            std::shared_ptr<RunLoop> rl,
+            int sock,
+            uint32_t flags = 0,
+            const std::optional<std::string> &trusted_pem_path = std::nullopt);
+
+    ~SSLSocket() override;
+
+    SSLSocket(const SSLSocket &) = delete;
+    SSLSocket &operator=(const SSLSocket &) = delete;
+
+    void postRecv(RunLoop::AsyncFunction fn) override;
+    void postSend(RunLoop::AsyncFunction fn) override;
+
+    ssize_t recvfrom(
+            void *data,
+            size_t size,
+            sockaddr *address,
+            socklen_t *addressLen) override;
+
+    ssize_t sendto(
+            const void *data,
+            size_t size,
+            const sockaddr *addr,
+            socklen_t addrLen) override;
+
+    void postFlush(RunLoop::AsyncFunction fn) override;
+
+private:
+    enum class Mode {
+        CONNECT,
+        ACCEPT,
+    };
+
+    Mode mMode;
+    uint32_t mFlags;
+
+    std::unique_ptr<SSL_CTX, std::function<void(SSL_CTX *)>> mCtx;
+    std::unique_ptr<SSL, std::function<void(SSL *)>> mSSL;
+
+    // These are owned by the SSL object.
+    BIO *mBioR;
+    BIO *mBioW;
+
+    bool mEOS;
+    int mFinalErrno;
+
+    bool mRecvPending;
+    RunLoop::AsyncFunction mRecvCallback;
+
+    bool mSendPending;
+    std::vector<uint8_t> mOutBuffer;
+
+    // This is as yet unencrypted data that has yet to be submitted to SSL_write.
+    std::vector<uint8_t> mOutBufferPlain;
+
+    RunLoop::AsyncFunction mFlushFn;
+
+    explicit SSLSocket(
+            std::shared_ptr<RunLoop> rl, Mode mode, int sock, uint32_t flags);
+
+    void handleIncomingData();
+    void sendRecvCallback();
+
+    void queueOutputDataFromSSL();
+
+    void queueOutputData(const void *data, size_t size);
+    void sendOutputData();
+
+    void drainOutputBufferPlain();
+    bool isPeerCertificateValid();
+
+    static SSL_CTX *CreateSSLContext();
+
+    bool useCertificate(const std::string &path);
+    bool usePrivateKey(const std::string &path);
+    bool useTrustedCertificates(const std::string &path);
+};
diff --git a/host/frontend/gcastv2/https/include/https/SafeCallbackable.h b/host/frontend/gcastv2/https/include/https/SafeCallbackable.h
new file mode 100644
index 0000000..26fdff8
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/SafeCallbackable.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <memory>
+
+template<typename T>
+std::function<void()> makeSafeCallback(T *obj, std::function<void(T *)> f) {
+    auto weak_me = std::weak_ptr<T>(obj->shared_from_this());
+    return [f, weak_me]{
+        auto me = weak_me.lock();
+        if (me) {
+            f(me.get());
+        }
+    };
+}
+
+template<typename T, typename... Params>
+std::function<void()> makeSafeCallback(
+        T *obj, void (T::*f)(const Params&...), const Params&... params) {
+    return makeSafeCallback<T>(
+            obj,
+            [f, params...](T *me) {
+                (me->*f)(params...);
+            });
+}
+
+template<typename T, typename... Params>
+std::function<void()> makeSafeCallback(
+        T *obj, void (T::*f)(Params...), const Params&... params) {
+    return makeSafeCallback<T>(
+            obj,
+            [f, params...](T *me) {
+                (me->*f)(params...);
+            });
+}
diff --git a/host/frontend/gcastv2/https/include/https/ServerSocket.h b/host/frontend/gcastv2/https/include/https/ServerSocket.h
new file mode 100644
index 0000000..4a6e41e
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/ServerSocket.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+struct ClientSocket;
+struct HTTPServer;
+struct RunLoop;
+
+struct ServerSocket : public std::enable_shared_from_this<ServerSocket> {
+    enum class TransportType {
+        TCP,
+        TLS,
+    };
+
+    explicit ServerSocket(
+            HTTPServer *server,
+            TransportType transportType,
+            const char *iface,
+            uint16_t port,
+            const std::optional<std::string> &certificate_pem_path,
+            const std::optional<std::string> &private_key_pem_path);
+
+    ~ServerSocket();
+
+    ServerSocket(const ServerSocket &) = delete;
+    ServerSocket &operator=(const ServerSocket &) = delete;
+
+    int initCheck() const;
+
+    TransportType transportType() const;
+
+    int run(std::shared_ptr<RunLoop> rl);
+
+    void onClientSocketClosed(int sock);
+
+    std::optional<std::string> certificate_pem_path() const;
+    std::optional<std::string> private_key_pem_path() const;
+
+private:
+    int mInitCheck;
+    HTTPServer *mServer;
+    std::optional<std::string> mCertificatePath, mPrivateKeyPath;
+    int mSocket;
+    TransportType mTransportType;
+
+    std::shared_ptr<RunLoop> mRunLoop;
+    std::vector<std::shared_ptr<ClientSocket>> mClientSockets;
+
+    void acceptIncomingConnection();
+};
+
diff --git a/host/frontend/gcastv2/https/include/https/Support.h b/host/frontend/gcastv2/https/include/https/Support.h
new file mode 100644
index 0000000..54892a6
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/Support.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <sys/types.h>
+
+#ifdef NDEBUG
+#define DEBUG_ONLY(x)
+#else
+#define DEBUG_ONLY(x)   x
+#endif
+
+void makeFdNonblocking(int fd);
+void hexdump(const void *_data, size_t size);
+
diff --git a/host/frontend/gcastv2/https/include/https/WebSocketHandler.h b/host/frontend/gcastv2/https/include/https/WebSocketHandler.h
new file mode 100644
index 0000000..82a5f0c
--- /dev/null
+++ b/host/frontend/gcastv2/https/include/https/WebSocketHandler.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <arpa/inet.h>
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <string>
+#include <sys/types.h>
+
+struct ClientSocket;
+
+struct WebSocketHandler {
+    virtual ~WebSocketHandler() = default;
+
+    // Returns number bytes processed or error.
+    ssize_t handleRequest(uint8_t *data, size_t size, bool isEOS);
+
+    bool isConnected();
+
+    virtual void setClientSocket(std::weak_ptr<ClientSocket> client);
+
+    typedef std::function<void(const uint8_t *, size_t)> OutputCallback;
+    void setOutputCallback(const sockaddr_in &remoteAddr, OutputCallback fn);
+
+    enum class SendMode {
+        text,
+        binary,
+        closeConnection,
+    };
+    int sendMessage(
+            const void *data, size_t size, SendMode mode = SendMode::text);
+
+    std::string remoteHost() const;
+
+protected:
+    virtual int handleMessage(
+            uint8_t headerByte, const uint8_t *msg, size_t len) = 0;
+
+private:
+    std::weak_ptr<ClientSocket> mClientSocket;
+
+    OutputCallback mOutputCallback;
+    sockaddr_in mRemoteAddr;
+};
diff --git a/host/frontend/gcastv2/libandroid/AAtomizer.cpp b/host/frontend/gcastv2/libandroid/AAtomizer.cpp
new file mode 100644
index 0000000..f8486c6
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/AAtomizer.cpp
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+#include <sys/types.h>
+
+#include <media/stagefright/foundation/AAtomizer.h>
+
+namespace android {
+
+// static
+AAtomizer AAtomizer::gAtomizer;
+
+// static
+const char *AAtomizer::Atomize(const char *name) {
+    return gAtomizer.atomize(name);
+}
+
+AAtomizer::AAtomizer() {
+    for (size_t i = 0; i < 128; ++i) {
+        mAtoms.push_back(std::list<std::string>());
+    }
+}
+
+const char *AAtomizer::atomize(const char *name) {
+    Mutex::Autolock autoLock(mLock);
+
+    const size_t n = mAtoms.size();
+    size_t index = AAtomizer::Hash(name) % n;
+    std::list<std::string> &entry = mAtoms[index];
+    std::list<std::string>::iterator it = entry.begin();
+    while (it != entry.end()) {
+        if ((*it) == name) {
+            return (*it).c_str();
+        }
+        ++it;
+    }
+
+    entry.push_back(std::string(name));
+
+    return (*--entry.end()).c_str();
+}
+
+// static
+uint32_t AAtomizer::Hash(const char *s) {
+    uint32_t sum = 0;
+    while (*s != '\0') {
+        sum = (sum * 31) + *s;
+        ++s;
+    }
+
+    return sum;
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/ABitReader.cpp b/host/frontend/gcastv2/libandroid/ABitReader.cpp
new file mode 100644
index 0000000..efb5385
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/ABitReader.cpp
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+
+#include <media/stagefright/foundation/ABitReader.h>
+
+#include <media/stagefright/foundation/ADebug.h>
+
+namespace android {
+
+ABitReader::ABitReader(const uint8_t *data, size_t size)
+    : mData(data),
+      mSize(size),
+      mReservoir(0),
+      mNumBitsLeft(0) {
+}
+
+void ABitReader::fillReservoir() {
+    CHECK_GT(mSize, 0u);
+
+    mReservoir = 0;
+    size_t i;
+    for (i = 0; mSize > 0 && i < 4; ++i) {
+        mReservoir = (mReservoir << 8) | *mData;
+
+        ++mData;
+        --mSize;
+    }
+
+    mNumBitsLeft = 8 * i;
+    mReservoir <<= 32 - mNumBitsLeft;
+}
+
+uint32_t ABitReader::getBits(size_t n) {
+    CHECK_LE(n, 32u);
+
+    uint32_t result = 0;
+    while (n > 0) {
+        if (mNumBitsLeft == 0) {
+            fillReservoir();
+        }
+
+        size_t m = n;
+        if (m > mNumBitsLeft) {
+            m = mNumBitsLeft;
+        }
+
+        result = (result << m) | (mReservoir >> (32 - m));
+        mReservoir <<= m;
+        mNumBitsLeft -= m;
+
+        n -= m;
+    }
+
+    return result;
+}
+
+void ABitReader::skipBits(size_t n) {
+    while (n > 32) {
+        getBits(32);
+        n -= 32;
+    }
+
+    if (n > 0) {
+        getBits(n);
+    }
+}
+
+void ABitReader::putBits(uint32_t x, size_t n) {
+    CHECK_LE(mNumBitsLeft + n, 32u);
+
+    mReservoir = (mReservoir >> n) | (x << (32 - n));
+    mNumBitsLeft += n;
+}
+
+size_t ABitReader::numBitsLeft() const {
+    return mSize * 8 + mNumBitsLeft;
+}
+
+const uint8_t *ABitReader::data() const {
+    CHECK_EQ(mNumBitsLeft % 8, 0u);
+
+    return mData - mNumBitsLeft / 8;
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/ABuffer.cpp b/host/frontend/gcastv2/libandroid/ABuffer.cpp
new file mode 100644
index 0000000..146bfaa
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/ABuffer.cpp
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+#include <media/stagefright/foundation/ABuffer.h>
+
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/ALooper.h>
+#include <media/stagefright/foundation/AMessage.h>
+
+namespace android {
+
+ABuffer::ABuffer(size_t capacity)
+    : mData(malloc(capacity)),
+      mCapacity(capacity),
+      mRangeOffset(0),
+      mRangeLength(capacity),
+      mInt32Data(0),
+      mOwnsData(true) {
+}
+
+ABuffer::ABuffer(void *data, size_t capacity)
+    : mData(data),
+      mCapacity(capacity),
+      mRangeOffset(0),
+      mRangeLength(capacity),
+      mInt32Data(0),
+      mOwnsData(false) {
+}
+
+ABuffer::~ABuffer() {
+    if (mOwnsData) {
+        if (mData != NULL) {
+            free(mData);
+            mData = NULL;
+        }
+    }
+
+    if (mFarewell != NULL) {
+        mFarewell->post();
+    }
+}
+
+void ABuffer::setRange(size_t offset, size_t size) {
+    CHECK_LE(offset, mCapacity);
+    CHECK_LE(offset + size, mCapacity);
+
+    mRangeOffset = offset;
+    mRangeLength = size;
+}
+
+void ABuffer::setFarewellMessage(const sp<AMessage> msg) {
+    mFarewell = msg;
+}
+
+sp<AMessage> ABuffer::meta() {
+    if (mMeta == NULL) {
+        mMeta = new AMessage;
+    }
+    return mMeta;
+}
+
+void ABuffer::reserve(size_t size) {
+    CHECK(mOwnsData);
+
+    if (mCapacity >= size) {
+        return;
+    }
+
+    mCapacity = size;
+    void *newData = realloc(mData, mCapacity);
+    if (!newData) {
+        newData = malloc(mCapacity);
+
+        memcpy(static_cast<uint8_t *>(newData) + mRangeOffset,
+               this->data(),
+               this->size());
+
+        free(mData);
+    }
+    mData = newData;
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/ADebug.cpp b/host/frontend/gcastv2/libandroid/ADebug.cpp
new file mode 100644
index 0000000..fae0970
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/ADebug.cpp
@@ -0,0 +1,61 @@
+
+#include <media/stagefright/foundation/ADebug.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#ifdef ANDROID
+#include <cutils/log.h>
+#endif
+
+namespace android {
+
+Logger::Logger(LogType type)
+    : mLogType(type) {
+    switch (mLogType) {
+        case VERBOSE:
+            mMessage = "V ";
+            break;
+        case INFO:
+            mMessage = "I ";
+            break;
+        case WARNING:
+            mMessage = "W ";
+            break;
+        case ERROR:
+            mMessage = "E ";
+            break;
+        case FATAL:
+            mMessage = "F ";
+            break;
+
+        default:
+            break;
+    }
+}
+
+Logger::~Logger() {
+    mMessage.append("\n");
+
+#if defined(TARGET_ANDROID_DEVICE)
+    if (mLogType == VERBOSE) {
+        return;
+    }
+
+    LOG_PRI(ANDROID_LOG_INFO, "ADebug", "%s", mMessage.c_str());
+#else
+    fprintf(stderr, "%s", mMessage.c_str());
+    fflush(stderr);
+#endif
+
+    if (mLogType == FATAL) {
+        abort();
+    }
+}
+
+const char *LeafName(const char *s) {
+    const char *lastSlash = strrchr(s, '/');
+    return lastSlash != NULL ? lastSlash + 1 : s;
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/ALooper.cpp b/host/frontend/gcastv2/libandroid/ALooper.cpp
new file mode 100644
index 0000000..136bdd9
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/ALooper.cpp
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "ALooper"
+#include <utils/Log.h>
+
+#include <time.h>
+
+#include <media/stagefright/foundation/ALooper.h>
+
+#include <media/stagefright/foundation/AHandler.h>
+#include <media/stagefright/foundation/ALooperRoster.h>
+#include <media/stagefright/foundation/AMessage.h>
+
+namespace android {
+
+ALooperRoster gLooperRoster;
+
+struct ALooper::LooperThread : public Thread {
+    LooperThread(ALooper *looper)
+        : mLooper(looper) {
+    }
+
+    virtual bool threadLoop() {
+        return mLooper->loop();
+    }
+
+protected:
+    virtual ~LooperThread() {}
+
+private:
+    ALooper *mLooper;
+
+    DISALLOW_EVIL_CONSTRUCTORS(LooperThread);
+};
+
+// static
+int64_t ALooper::GetNowUs() {
+    struct timespec ts;
+    int res = clock_gettime(CLOCK_MONOTONIC, &ts);
+    CHECK(!res);
+
+    return static_cast<int64_t>(ts.tv_sec) * 1000000
+        + (ts.tv_nsec + 500) / 1000;
+}
+
+ALooper::ALooper()
+    : mRunningLocally(false) {
+}
+
+ALooper::~ALooper() {
+    stop();
+}
+
+ALooper::handler_id ALooper::registerHandler(const sp<AHandler> &handler) {
+    return gLooperRoster.registerHandler(this, handler);
+}
+
+void ALooper::unregisterHandler(handler_id handlerID) {
+    gLooperRoster.unregisterHandler(handlerID);
+}
+
+status_t ALooper::start(bool runOnCallingThread) {
+    if (runOnCallingThread) {
+        {
+            Mutex::Autolock autoLock(mLock);
+
+            if (mThread != NULL || mRunningLocally) {
+                return INVALID_OPERATION;
+            }
+
+            mRunningLocally = true;
+        }
+
+        do {
+        } while (loop());
+
+        return OK;
+    }
+
+    Mutex::Autolock autoLock(mLock);
+
+    if (mThread != NULL || mRunningLocally) {
+        return INVALID_OPERATION;
+    }
+
+    mThread = new LooperThread(this);
+
+    status_t err = mThread->run("ALooper");
+    if (err != OK) {
+        mThread.clear();
+    }
+
+    return err;
+}
+
+status_t ALooper::stop() {
+    sp<LooperThread> thread;
+    bool runningLocally;
+
+    {
+        Mutex::Autolock autoLock(mLock);
+
+        thread = mThread;
+        runningLocally = mRunningLocally;
+        mThread.clear();
+        mRunningLocally = false;
+    }
+
+    if (thread == NULL && !runningLocally) {
+        return INVALID_OPERATION;
+    }
+
+    mQueueChangedCondition.signal();
+
+    if (thread != NULL) {
+        thread->requestExit();
+    }
+
+    return OK;
+}
+
+void ALooper::post(const sp<AMessage> &msg, int64_t delayUs) {
+    Mutex::Autolock autoLock(mLock);
+
+    int64_t whenUs;
+    if (delayUs > 0) {
+        whenUs = GetNowUs() + delayUs;
+    } else {
+        whenUs = GetNowUs();
+    }
+
+    auto it = mEventQueue.begin();
+    while (it != mEventQueue.end() && (*it).mWhenUs <= whenUs) {
+        ++it;
+    }
+
+    Event event;
+    event.mWhenUs = whenUs;
+    event.mMessage = msg;
+
+    if (it == mEventQueue.begin()) {
+        mQueueChangedCondition.signal();
+    }
+
+    mEventQueue.insert(it, event);
+}
+
+bool ALooper::loop() {
+    Event event;
+
+    {
+        Mutex::Autolock autoLock(mLock);
+        if (mThread == NULL && !mRunningLocally) {
+            return false;
+        }
+        if (mEventQueue.empty()) {
+            mQueueChangedCondition.wait(mLock);
+            return true;
+        }
+        int64_t whenUs = (*mEventQueue.begin()).mWhenUs;
+        int64_t nowUs = GetNowUs();
+
+        if (whenUs > nowUs) {
+            int64_t delayUs = whenUs - nowUs;
+            mQueueChangedCondition.waitRelative(mLock, delayUs * 1000ll);
+
+            return true;
+        }
+
+        event = mEventQueue.front();
+        mEventQueue.pop_front();
+    }
+
+    gLooperRoster.deliverMessage(event.mMessage);
+
+    // NOTE: It's important to note that at this point our "ALooper" object
+    // may no longer exist (its final reference may have gone away while
+    // delivering the message). We have made sure, however, that loop()
+    // won't be called again.
+
+    return true;
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/ALooperRoster.cpp b/host/frontend/gcastv2/libandroid/ALooperRoster.cpp
new file mode 100644
index 0000000..d701c9e
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/ALooperRoster.cpp
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "ALooperRoster"
+#include <utils/Log.h>
+
+#include <media/stagefright/foundation/ALooperRoster.h>
+
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/AHandler.h>
+#include <media/stagefright/foundation/AMessage.h>
+
+namespace android {
+
+ALooperRoster::ALooperRoster()
+    : mNextHandlerID(1) {
+}
+
+ALooper::handler_id ALooperRoster::registerHandler(
+        const sp<ALooper> looper, const sp<AHandler> &handler) {
+    Mutex::Autolock autoLock(mLock);
+
+    if (handler->id() != 0) {
+        CHECK(!"A handler must only be registered once.");
+        return INVALID_OPERATION;
+    }
+
+    handler->mLooper = looper.get();
+
+    HandlerInfo info;
+    info.mLooper = looper;
+    info.mHandler = handler.get();
+    ALooper::handler_id handlerID = mNextHandlerID++;
+    mHandlers[handlerID] = info;
+
+    handler->setID(handlerID);
+
+    return handlerID;
+}
+
+void ALooperRoster::unregisterHandler(ALooper::handler_id handlerID) {
+    Mutex::Autolock autoLock(mLock);
+
+    auto it = mHandlers.find(handlerID);
+    CHECK(it != mHandlers.end());
+
+    const HandlerInfo &info = it->second;
+    sp<AHandler> handler = info.mHandler.promote();
+
+    if (handler != NULL) {
+        handler->setID(0);
+    }
+
+    mHandlers.erase(it);
+}
+
+void ALooperRoster::postMessage(
+        const sp<AMessage> &msg, int64_t delayUs) {
+    Mutex::Autolock autoLock(mLock);
+
+    auto it = mHandlers.find(msg->target());
+
+    if (it == mHandlers.end()) {
+        LOG(WARNING) << "failed to post message. Target handler not registered.";
+        return;
+    }
+
+    const HandlerInfo &info = it->second;
+    info.mLooper->post(msg, delayUs);
+}
+
+void ALooperRoster::deliverMessage(const sp<AMessage> &msg) {
+    sp<AHandler> handler;
+
+    {
+        Mutex::Autolock autoLock(mLock);
+
+        auto it = mHandlers.find(msg->target());
+
+        if (it == mHandlers.end()) {
+            LOG(WARNING) << "failed to deliver message. Target handler not registered.";
+            return;
+        }
+
+        const HandlerInfo &info = it->second;
+        handler = info.mHandler.promote();
+
+        if (handler == NULL) {
+            mHandlers.erase(it);
+            return;
+        }
+    }
+
+    handler->onMessageReceived(msg);
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/AMessage.cpp b/host/frontend/gcastv2/libandroid/AMessage.cpp
new file mode 100644
index 0000000..93d8f84
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/AMessage.cpp
@@ -0,0 +1,388 @@
+/*
+ * 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.
+ */
+
+#include <media/stagefright/foundation/AMessage.h>
+
+#include <media/stagefright/Utils.h>
+#include <media/stagefright/foundation/AAtomizer.h>
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/ALooperRoster.h>
+#include <media/stagefright/foundation/hexdump.h>
+
+namespace android {
+
+AMessage::AMessage(uint32_t what, ALooper::handler_id target)
+    : mWhat(what),
+      mTarget(target),
+      mNumItems(0) {
+}
+
+AMessage::~AMessage() {
+    clear();
+}
+
+void AMessage::setWhat(uint32_t what) {
+    mWhat = what;
+}
+
+uint32_t AMessage::what() const {
+    return mWhat;
+}
+
+void AMessage::setTarget(ALooper::handler_id handlerID) {
+    mTarget = handlerID;
+}
+
+ALooper::handler_id AMessage::target() const {
+    return mTarget;
+}
+
+void AMessage::clear() {
+    for (size_t i = 0; i < mNumItems; ++i) {
+        Item *item = &mItems[i];
+        freeItem(item);
+    }
+    mNumItems = 0;
+}
+
+void AMessage::freeItem(Item *item) {
+    switch (item->mType) {
+        case kTypeString:
+        {
+            delete item->u.stringValue;
+            break;
+        }
+
+        case kTypeObject:
+        case kTypeMessage:
+        case kTypeBuffer:
+        {
+            if (item->u.refValue != NULL) {
+                item->u.refValue->decStrong(this);
+            }
+            break;
+        }
+
+        default:
+            break;
+    }
+}
+
+AMessage::Item *AMessage::allocateItem(const char *name) {
+    name = AAtomizer::Atomize(name);
+
+    size_t i = 0;
+    while (i < mNumItems && mItems[i].mName != name) {
+        ++i;
+    }
+
+    Item *item;
+
+    if (i < mNumItems) {
+        item = &mItems[i];
+        freeItem(item);
+    } else {
+        CHECK(mNumItems < kMaxNumItems);
+        i = mNumItems++;
+        item = &mItems[i];
+
+        item->mName = name;
+    }
+
+    return item;
+}
+
+const AMessage::Item *AMessage::findItem(
+        const char *name, Type type) const {
+    name = AAtomizer::Atomize(name);
+
+    for (size_t i = 0; i < mNumItems; ++i) {
+        const Item *item = &mItems[i];
+
+        if (item->mName == name) {
+            return item->mType == type ? item : NULL;
+        }
+    }
+
+    return NULL;
+}
+
+#define BASIC_TYPE(NAME,FIELDNAME,TYPENAME)                             \
+void AMessage::set##NAME(const char *name, TYPENAME value) {            \
+    Item *item = allocateItem(name);                                    \
+                                                                        \
+    item->mType = kType##NAME;                                          \
+    item->u.FIELDNAME = value;                                          \
+}                                                                       \
+                                                                        \
+bool AMessage::find##NAME(const char *name, TYPENAME *value) const {    \
+    const Item *item = findItem(name, kType##NAME);                     \
+    if (item) {                                                         \
+        *value = item->u.FIELDNAME;                                     \
+        return true;                                                    \
+    }                                                                   \
+    return false;                                                       \
+}
+
+BASIC_TYPE(Int32,int32Value,int32_t)
+BASIC_TYPE(Int64,int64Value,int64_t)
+BASIC_TYPE(Size,sizeValue,size_t)
+BASIC_TYPE(Float,floatValue,float)
+BASIC_TYPE(Double,doubleValue,double)
+BASIC_TYPE(Pointer,ptrValue,void *)
+
+#undef BASIC_TYPE
+
+void AMessage::setString(
+        const char *name, const char *s, ssize_t len) {
+    Item *item = allocateItem(name);
+    item->mType = kTypeString;
+    item->u.stringValue = new std::string(s, len < 0 ? strlen(s) : len);
+}
+
+void AMessage::setObject(const char *name, const sp<RefBase> &obj) {
+    Item *item = allocateItem(name);
+    item->mType = kTypeObject;
+
+    if (obj != NULL) { obj->incStrong(this); }
+    item->u.refValue = obj.get();
+}
+
+void AMessage::setMessage(const char *name, const sp<AMessage> &obj) {
+    Item *item = allocateItem(name);
+    item->mType = kTypeMessage;
+
+    if (obj != NULL) { obj->incStrong(this); }
+    item->u.refValue = obj.get();
+}
+
+void AMessage::setBuffer(const char *name, const sp<ABuffer> &obj) {
+    Item *item = allocateItem(name);
+    item->mType = kTypeBuffer;
+
+    if (obj != NULL) { obj->incStrong(this); }
+    item->u.refValue = obj.get();
+}
+
+bool AMessage::findString(const char *name, std::string *value) const {
+    const Item *item = findItem(name, kTypeString);
+    if (item) {
+        *value = *item->u.stringValue;
+        return true;
+    }
+    return false;
+}
+
+bool AMessage::findObject(const char *name, sp<RefBase> *obj) const {
+    const Item *item = findItem(name, kTypeObject);
+    if (item) {
+        *obj = item->u.refValue;
+        return true;
+    }
+    return false;
+}
+
+bool AMessage::findMessage(const char *name, sp<AMessage> *obj) const {
+    const Item *item = findItem(name, kTypeMessage);
+    if (item) {
+        *obj = static_cast<AMessage *>(item->u.refValue);
+        return true;
+    }
+    return false;
+}
+
+bool AMessage::findBuffer(const char *name, sp<ABuffer> *obj) const {
+    const Item *item = findItem(name, kTypeBuffer);
+    if (item) {
+        *obj = static_cast<ABuffer *>(item->u.refValue);
+        return true;
+    }
+    return false;
+}
+
+void AMessage::post(int64_t delayUs) {
+    extern ALooperRoster gLooperRoster;
+
+    gLooperRoster.postMessage(this, delayUs);
+}
+
+sp<AMessage> AMessage::dup() const {
+    sp<AMessage> msg = new AMessage(mWhat, mTarget);
+    msg->mNumItems = mNumItems;
+
+    for (size_t i = 0; i < mNumItems; ++i) {
+        const Item *from = &mItems[i];
+        Item *to = &msg->mItems[i];
+
+        to->mName = from->mName;
+        to->mType = from->mType;
+
+        switch (from->mType) {
+            case kTypeString:
+            {
+                to->u.stringValue = new std::string(*from->u.stringValue);
+                break;
+            }
+
+            case kTypeObject:
+            case kTypeMessage:
+            case kTypeBuffer:
+            {
+                to->u.refValue = from->u.refValue;
+                to->u.refValue->incStrong(this);
+                break;
+            }
+
+            default:
+            {
+                to->u = from->u;
+                break;
+            }
+        }
+    }
+
+    return msg;
+}
+
+static bool isFourcc(uint32_t what) {
+    return isprint(what & 0xff)
+        && isprint((what >> 8) & 0xff)
+        && isprint((what >> 16) & 0xff)
+        && isprint((what >> 24) & 0xff);
+}
+
+static void appendIndent(std::string *s, size_t indent) {
+    static const char kWhitespace[] =
+        "                                        "
+        "                                        ";
+
+    CHECK_LT((size_t)indent, sizeof(kWhitespace));
+
+    s->append(kWhitespace, indent);
+}
+
+std::string AMessage::debugString(size_t indent) const {
+    std::string s = "AMessage(what = ";
+
+    std::string tmp;
+    if (isFourcc(mWhat)) {
+        tmp = StringPrintf(
+                "'%c%c%c%c'",
+                (char)(mWhat >> 24),
+                (char)((mWhat >> 16) & 0xff),
+                (char)((mWhat >> 8) & 0xff),
+                (char)(mWhat & 0xff));
+    } else {
+        tmp = StringPrintf("0x%08x", mWhat);
+    }
+    s.append(tmp);
+
+    if (mTarget != 0) {
+        tmp = StringPrintf(", target = %d", mTarget);
+        s.append(tmp);
+    }
+    s.append(") = {\n");
+
+    for (size_t i = 0; i < mNumItems; ++i) {
+        const Item &item = mItems[i];
+
+        switch (item.mType) {
+            case kTypeInt32:
+                tmp = StringPrintf(
+                        "int32_t %s = %d", item.mName, item.u.int32Value);
+                break;
+            case kTypeInt64:
+                tmp = StringPrintf(
+                        "int64_t %s = %lld", item.mName, item.u.int64Value);
+                break;
+            case kTypeSize:
+                tmp = StringPrintf(
+                        "size_t %s = %d", item.mName, item.u.sizeValue);
+                break;
+            case kTypeFloat:
+                tmp = StringPrintf(
+                        "float %s = %f", item.mName, item.u.floatValue);
+                break;
+            case kTypeDouble:
+                tmp = StringPrintf(
+                        "double %s = %f", item.mName, item.u.doubleValue);
+                break;
+            case kTypePointer:
+                tmp = StringPrintf(
+                        "void *%s = %p", item.mName, item.u.ptrValue);
+                break;
+            case kTypeString:
+                tmp = StringPrintf(
+                        "string %s = \"%s\"",
+                        item.mName,
+                        item.u.stringValue->c_str());
+                break;
+            case kTypeObject:
+                tmp = StringPrintf(
+                        "RefBase *%s = %p", item.mName, item.u.refValue);
+                break;
+            case kTypeBuffer:
+            {
+                sp<ABuffer> buffer = static_cast<ABuffer *>(item.u.refValue);
+
+                if (buffer != NULL && buffer->data() != NULL && buffer->size() <= 1024) {
+                    tmp = StringPrintf("Buffer %s = {\n", item.mName);
+                    hexdump(buffer->data(), buffer->size(), indent + 4, &tmp);
+                    appendIndent(&tmp, indent + 2);
+                    tmp.append("}");
+                } else {
+                    tmp = StringPrintf(
+                            "Buffer *%s = %p", item.mName, buffer.get());
+                }
+                break;
+            }
+            case kTypeMessage:
+                tmp = StringPrintf(
+                        "AMessage %s = %s",
+                        item.mName,
+                        static_cast<AMessage *>(
+                            item.u.refValue)->debugString(
+                                indent + strlen(item.mName) + 14).c_str());
+                break;
+            default:
+                TRESPASS();
+        }
+
+        appendIndent(&s, indent);
+        s.append("  ");
+        s.append(tmp);
+        s.append("\n");
+    }
+
+    appendIndent(&s, indent);
+    s.append("}");
+
+    return s;
+}
+
+size_t AMessage::countEntries() const {
+    return mNumItems;
+}
+
+const char *AMessage::getEntryNameAt(size_t i, Type *type) const {
+    CHECK_LT(i, mNumItems);
+    *type = mItems[i].mType;
+
+    return mItems[i].mName;
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/ANetworkSession.cpp b/host/frontend/gcastv2/libandroid/ANetworkSession.cpp
new file mode 100644
index 0000000..f422a34
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/ANetworkSession.cpp
@@ -0,0 +1,1410 @@
+/*
+ * 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 "NetworkSession"
+#include <utils/Log.h>
+
+#include <media/stagefright/foundation/ANetworkSession.h>
+
+#include <arpa/inet.h>
+#include <fcntl.h>
+#include <net/if.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <media/stagefright/Utils.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/foundation/ParsedMessage.h>
+
+#include <list>
+
+namespace android {
+
+static const size_t kMaxUDPSize = 1500;
+static const int32_t kMaxUDPRetries = 200;
+
+struct ANetworkSession::NetworkThread : public Thread {
+    NetworkThread(ANetworkSession *session);
+
+protected:
+    virtual ~NetworkThread();
+
+private:
+    ANetworkSession *mSession;
+
+    virtual bool threadLoop();
+
+    DISALLOW_EVIL_CONSTRUCTORS(NetworkThread);
+};
+
+struct ANetworkSession::Session : public RefBase {
+    enum Mode {
+        MODE_RTSP,
+        MODE_DATAGRAM,
+        MODE_WEBSOCKET,
+    };
+
+    enum State {
+        CONNECTING,
+        CONNECTED,
+        LISTENING_RTSP,
+        LISTENING_TCP_DGRAMS,
+        DATAGRAM,
+    };
+
+    Session(int32_t sessionID,
+            State state,
+            int s,
+            const sp<AMessage> &notify);
+
+    int32_t sessionID() const;
+    int socket() const;
+    sp<AMessage> getNotificationMessage() const;
+
+    bool isRTSPServer() const;
+    bool isTCPDatagramServer() const;
+
+    bool wantsToRead();
+    bool wantsToWrite();
+
+    status_t readMore();
+    status_t writeMore();
+
+    status_t sendRequest(
+            const void *data, ssize_t size, bool timeValid, int64_t timeUs);
+
+    void setMode(Mode mode);
+
+    status_t switchToWebSocketMode();
+
+protected:
+    virtual ~Session();
+
+private:
+    enum {
+        FRAGMENT_FLAG_TIME_VALID = 1,
+    };
+    struct Fragment {
+        uint32_t mFlags;
+        int64_t mTimeUs;
+        sp<ABuffer> mBuffer;
+    };
+
+    int32_t mSessionID;
+    State mState;
+    Mode mMode;
+    int mSocket;
+    sp<AMessage> mNotify;
+    bool mSawReceiveFailure, mSawSendFailure;
+    int32_t mUDPRetries;
+
+    std::list<Fragment> mOutFragments;
+
+    std::string mInBuffer;
+
+    int64_t mLastStallReportUs;
+
+    void notifyError(bool send, status_t err, const char *detail);
+    void notify(NotificationReason reason);
+
+    void dumpFragmentStats(const Fragment &frag);
+
+    DISALLOW_EVIL_CONSTRUCTORS(Session);
+};
+////////////////////////////////////////////////////////////////////////////////
+
+ANetworkSession::NetworkThread::NetworkThread(ANetworkSession *session)
+    : mSession(session) {
+}
+
+ANetworkSession::NetworkThread::~NetworkThread() {
+}
+
+bool ANetworkSession::NetworkThread::threadLoop() {
+    mSession->threadLoop();
+
+    return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ANetworkSession::Session::Session(
+        int32_t sessionID,
+        State state,
+        int s,
+        const sp<AMessage> &notify)
+    : mSessionID(sessionID),
+      mState(state),
+      mMode(MODE_DATAGRAM),
+      mSocket(s),
+      mNotify(notify),
+      mSawReceiveFailure(false),
+      mSawSendFailure(false),
+      mUDPRetries(kMaxUDPRetries),
+      mLastStallReportUs(-1ll) {
+    if (mState == CONNECTED) {
+        struct sockaddr_in localAddr;
+        socklen_t localAddrLen = sizeof(localAddr);
+
+        int res = getsockname(
+                mSocket, (struct sockaddr *)&localAddr, &localAddrLen);
+        CHECK_GE(res, 0);
+
+        struct sockaddr_in remoteAddr;
+        socklen_t remoteAddrLen = sizeof(remoteAddr);
+
+        res = getpeername(
+                mSocket, (struct sockaddr *)&remoteAddr, &remoteAddrLen);
+        CHECK_GE(res, 0);
+
+        in_addr_t addr = ntohl(localAddr.sin_addr.s_addr);
+        std::string localAddrString = StringPrintf(
+                "%d.%d.%d.%d",
+                (addr >> 24),
+                (addr >> 16) & 0xff,
+                (addr >> 8) & 0xff,
+                addr & 0xff);
+
+        addr = ntohl(remoteAddr.sin_addr.s_addr);
+        std::string remoteAddrString = StringPrintf(
+                "%d.%d.%d.%d",
+                (addr >> 24),
+                (addr >> 16) & 0xff,
+                (addr >> 8) & 0xff,
+                addr & 0xff);
+
+        sp<AMessage> msg = mNotify->dup();
+        msg->setInt32("sessionID", mSessionID);
+        msg->setInt32("reason", kWhatClientConnected);
+        msg->setString("server-ip", localAddrString.c_str());
+        msg->setInt32("server-port", ntohs(localAddr.sin_port));
+        msg->setString("client-ip", remoteAddrString.c_str());
+        msg->setInt32("client-port", ntohs(remoteAddr.sin_port));
+        msg->post();
+    }
+}
+
+ANetworkSession::Session::~Session() {
+    ALOGV("Session %d gone", mSessionID);
+
+    close(mSocket);
+    mSocket = -1;
+}
+
+int32_t ANetworkSession::Session::sessionID() const {
+    return mSessionID;
+}
+
+int ANetworkSession::Session::socket() const {
+    return mSocket;
+}
+
+void ANetworkSession::Session::setMode(Mode mode) {
+    mMode = mode;
+}
+
+status_t ANetworkSession::Session::switchToWebSocketMode() {
+    if (mState != CONNECTED || mMode != MODE_RTSP) {
+        return INVALID_OPERATION;
+    }
+
+    mMode = MODE_WEBSOCKET;
+
+    return OK;
+}
+
+sp<AMessage> ANetworkSession::Session::getNotificationMessage() const {
+    return mNotify;
+}
+
+bool ANetworkSession::Session::isRTSPServer() const {
+    return mState == LISTENING_RTSP;
+}
+
+bool ANetworkSession::Session::isTCPDatagramServer() const {
+    return mState == LISTENING_TCP_DGRAMS;
+}
+
+bool ANetworkSession::Session::wantsToRead() {
+    return !mSawReceiveFailure && mState != CONNECTING;
+}
+
+bool ANetworkSession::Session::wantsToWrite() {
+    return !mSawSendFailure
+        && (mState == CONNECTING
+            || (mState == CONNECTED && !mOutFragments.empty())
+            || (mState == DATAGRAM && !mOutFragments.empty()));
+}
+
+status_t ANetworkSession::Session::readMore() {
+    if (mState == DATAGRAM) {
+        CHECK_EQ(mMode, MODE_DATAGRAM);
+
+        status_t err;
+        do {
+            sp<ABuffer> buf = new ABuffer(kMaxUDPSize);
+
+            struct sockaddr_in remoteAddr;
+            socklen_t remoteAddrLen = sizeof(remoteAddr);
+
+            ssize_t n;
+            do {
+                n = recvfrom(
+                        mSocket, buf->data(), buf->capacity(), 0,
+                        (struct sockaddr *)&remoteAddr, &remoteAddrLen);
+            } while (n < 0 && errno == EINTR);
+
+            err = OK;
+            if (n < 0) {
+                err = -errno;
+            } else if (n == 0) {
+                err = -ECONNRESET;
+            } else {
+                buf->setRange(0, n);
+
+                int64_t nowUs = ALooper::GetNowUs();
+                buf->meta()->setInt64("arrivalTimeUs", nowUs);
+
+                sp<AMessage> notify = mNotify->dup();
+                notify->setInt32("sessionID", mSessionID);
+                notify->setInt32("reason", kWhatDatagram);
+
+                uint32_t ip = ntohl(remoteAddr.sin_addr.s_addr);
+                notify->setString(
+                        "fromAddr",
+                        StringPrintf(
+                            "%u.%u.%u.%u",
+                            ip >> 24,
+                            (ip >> 16) & 0xff,
+                            (ip >> 8) & 0xff,
+                            ip & 0xff).c_str());
+
+                notify->setInt32("fromPort", ntohs(remoteAddr.sin_port));
+
+                notify->setBuffer("data", buf);
+                notify->post();
+            }
+        } while (err == OK);
+
+        if (err == -EAGAIN) {
+            err = OK;
+        }
+
+        if (err != OK) {
+            if (!mUDPRetries) {
+                notifyError(false /* send */, err, "Recvfrom failed.");
+                mSawReceiveFailure = true;
+            } else {
+                mUDPRetries--;
+                ALOGE("Recvfrom failed, %d/%d retries left",
+                        mUDPRetries, kMaxUDPRetries);
+                err = OK;
+            }
+        } else {
+            mUDPRetries = kMaxUDPRetries;
+        }
+
+        return err;
+    }
+
+    char tmp[512];
+    ssize_t n;
+    do {
+        n = recv(mSocket, tmp, sizeof(tmp), 0);
+    } while (n < 0 && errno == EINTR);
+
+    status_t err = OK;
+
+    if (n > 0) {
+        mInBuffer.append(tmp, n);
+
+#if 0
+        ALOGI("in:");
+        hexdump(tmp, n);
+#endif
+    } else if (n < 0) {
+        err = -errno;
+    } else {
+        err = -ECONNRESET;
+    }
+
+    if (mMode == MODE_DATAGRAM) {
+        // TCP stream carrying 16-bit length-prefixed datagrams.
+
+        while (mInBuffer.size() >= 2) {
+            size_t packetSize = U16_AT((const uint8_t *)mInBuffer.c_str());
+
+            if (mInBuffer.size() < packetSize + 2) {
+                break;
+            }
+
+            sp<ABuffer> packet = new ABuffer(packetSize);
+            memcpy(packet->data(), mInBuffer.c_str() + 2, packetSize);
+
+            int64_t nowUs = ALooper::GetNowUs();
+            packet->meta()->setInt64("arrivalTimeUs", nowUs);
+
+            sp<AMessage> notify = mNotify->dup();
+            notify->setInt32("sessionID", mSessionID);
+            notify->setInt32("reason", kWhatDatagram);
+            notify->setBuffer("data", packet);
+            notify->post();
+
+            mInBuffer.erase(0, packetSize + 2);
+        }
+    } else if (mMode == MODE_RTSP) {
+        for (;;) {
+            size_t length;
+
+            if (mInBuffer.size() > 0 && mInBuffer.c_str()[0] == '$') {
+                if (mInBuffer.size() < 4) {
+                    break;
+                }
+
+                length = U16_AT((const uint8_t *)mInBuffer.c_str() + 2);
+
+                if (mInBuffer.size() < 4 + length) {
+                    break;
+                }
+
+                sp<AMessage> notify = mNotify->dup();
+                notify->setInt32("sessionID", mSessionID);
+                notify->setInt32("reason", kWhatBinaryData);
+                notify->setInt32("channel", mInBuffer.c_str()[1]);
+
+                sp<ABuffer> data = new ABuffer(length);
+                memcpy(data->data(), mInBuffer.c_str() + 4, length);
+
+                int64_t nowUs = ALooper::GetNowUs();
+                data->meta()->setInt64("arrivalTimeUs", nowUs);
+
+                notify->setBuffer("data", data);
+                notify->post();
+
+                mInBuffer.erase(0, 4 + length);
+                continue;
+            }
+
+            sp<ParsedMessage> msg =
+                ParsedMessage::Parse(
+                        mInBuffer.c_str(), mInBuffer.size(), err != OK, &length);
+
+            if (msg == NULL) {
+                break;
+            }
+
+            sp<AMessage> notify = mNotify->dup();
+            notify->setInt32("sessionID", mSessionID);
+            notify->setInt32("reason", kWhatData);
+            notify->setObject("data", msg);
+            notify->post();
+
+#if 1
+            // XXX The (old) dongle sends the wrong content length header on a
+            // SET_PARAMETER request that signals a "wfd_idr_request".
+            // (17 instead of 19).
+            const char *content = msg->getContent();
+            if (content
+                    && !memcmp(content, "wfd_idr_request\r\n", 17)
+                    && length >= 19
+                    && mInBuffer.c_str()[length] == '\r'
+                    && mInBuffer.c_str()[length + 1] == '\n') {
+                length += 2;
+            }
+#endif
+
+            mInBuffer.erase(0, length);
+
+            if (err != OK) {
+                break;
+            }
+        }
+    } else {
+        CHECK_EQ(mMode, MODE_WEBSOCKET);
+
+        const uint8_t *data = (const uint8_t *)mInBuffer.c_str();
+        // hexdump(data, mInBuffer.size());
+
+        while (mInBuffer.size() >= 2) {
+            size_t offset = 2;
+
+            size_t payloadLen = data[1] & 0x7f;
+            if (payloadLen == 126) {
+                if (offset + 2 > mInBuffer.size()) {
+                    break;
+                }
+
+                payloadLen = U16_AT(&data[offset]);
+                offset += 2;
+            } else if (payloadLen == 127) {
+                if (offset + 8 > mInBuffer.size()) {
+                    break;
+                }
+
+                payloadLen = U64_AT(&data[offset]);
+                
+                offset += 8;
+            }
+
+            uint32_t mask = 0;
+            if (data[1] & 0x80) {
+                // MASK==1
+                if (offset + 4 > mInBuffer.size()) {
+                    break;
+                }
+
+                mask = U32_AT(&data[offset]);
+                offset += 4;
+            }
+
+            if (offset + payloadLen > mInBuffer.size()) {
+                break;
+            }
+
+            // We have the full message.
+
+            sp<ABuffer> packet = new ABuffer(payloadLen);
+            memcpy(packet->data(), &data[offset], payloadLen);
+
+            if (mask != 0) {
+                for (size_t i = 0; i < payloadLen; ++i) {
+                    packet->data()[i] =
+                        data[offset + i]
+                            ^ ((mask >> (8 * (3 - (i % 4)))) & 0xff);
+                }
+            }
+
+            sp<AMessage> notify = mNotify->dup();
+            notify->setInt32("sessionID", mSessionID);
+            notify->setInt32("reason", kWhatWebSocketMessage);
+            notify->setBuffer("data", packet);
+            notify->setInt32("headerByte", data[0]);
+            notify->post();
+
+            mInBuffer.erase(0, offset + payloadLen);
+        }
+    }
+
+    if (err != OK) {
+        notifyError(false /* send */, err, "Recv failed.");
+        mSawReceiveFailure = true;
+    }
+
+    return err;
+}
+
+void ANetworkSession::Session::dumpFragmentStats(const Fragment & /* frag */) {
+#if 0
+    int64_t nowUs = ALooper::GetNowUs();
+    int64_t delayMs = (nowUs - frag.mTimeUs) / 1000ll;
+
+    static const int64_t kMinDelayMs = 0;
+    static const int64_t kMaxDelayMs = 300;
+
+    const char *kPattern = "########################################";
+    size_t kPatternSize = strlen(kPattern);
+
+    int n = (kPatternSize * (delayMs - kMinDelayMs))
+                / (kMaxDelayMs - kMinDelayMs);
+
+    if (n < 0) {
+        n = 0;
+    } else if ((size_t)n > kPatternSize) {
+        n = kPatternSize;
+    }
+
+    ALOGI("[%lld]: (%4lld ms) %s\n",
+          frag.mTimeUs / 1000,
+          delayMs,
+          kPattern + kPatternSize - n);
+#endif
+}
+
+status_t ANetworkSession::Session::writeMore() {
+    if (mState == DATAGRAM) {
+        CHECK(!mOutFragments.empty());
+
+        status_t err;
+        do {
+            const Fragment &frag = *mOutFragments.begin();
+            const sp<ABuffer> &datagram = frag.mBuffer;
+
+            ssize_t n;
+            do {
+                n = send(mSocket, datagram->data(), datagram->size(), 0);
+            } while (n < 0 && errno == EINTR);
+
+            err = OK;
+
+            if (n > 0) {
+                if (frag.mFlags & FRAGMENT_FLAG_TIME_VALID) {
+                    dumpFragmentStats(frag);
+                }
+
+                mOutFragments.erase(mOutFragments.begin());
+            } else if (n < 0) {
+                err = -errno;
+            } else if (n == 0) {
+                err = -ECONNRESET;
+            }
+        } while (err == OK && !mOutFragments.empty());
+
+        if (err == -EAGAIN) {
+            if (!mOutFragments.empty()) {
+                ALOGI("%d datagrams remain queued.", mOutFragments.size());
+            }
+            err = OK;
+        }
+
+        if (err != OK) {
+            if (!mUDPRetries) {
+                notifyError(true /* send */, err, "Send datagram failed.");
+                mSawSendFailure = true;
+            } else {
+                mUDPRetries--;
+                ALOGE("Send datagram failed, %d/%d retries left",
+                        mUDPRetries, kMaxUDPRetries);
+                err = OK;
+            }
+        } else {
+            mUDPRetries = kMaxUDPRetries;
+        }
+
+        return err;
+    }
+
+    if (mState == CONNECTING) {
+        int err;
+        socklen_t optionLen = sizeof(err);
+        CHECK_EQ(getsockopt(mSocket, SOL_SOCKET, SO_ERROR, &err, &optionLen), 0);
+        CHECK_EQ(optionLen, (socklen_t)sizeof(err));
+
+        if (err != 0) {
+            notifyError(kWhatError, -err, "Connection failed");
+            mSawSendFailure = true;
+
+            return -err;
+        }
+
+        mState = CONNECTED;
+        notify(kWhatConnected);
+
+        return OK;
+    }
+
+    CHECK_EQ(mState, CONNECTED);
+    CHECK(!mOutFragments.empty());
+
+    ssize_t n = 0;
+    while (!mOutFragments.empty()) {
+        const Fragment &frag = *mOutFragments.begin();
+
+        do {
+            n = send(mSocket, frag.mBuffer->data(), frag.mBuffer->size(), 0);
+        } while (n < 0 && errno == EINTR);
+
+        if (n <= 0) {
+            break;
+        }
+
+        frag.mBuffer->setRange(
+                frag.mBuffer->offset() + n, frag.mBuffer->size() - n);
+
+        if (frag.mBuffer->size() > 0) {
+            break;
+        }
+
+        if (frag.mFlags & FRAGMENT_FLAG_TIME_VALID) {
+            dumpFragmentStats(frag);
+        }
+
+        mOutFragments.erase(mOutFragments.begin());
+    }
+
+    status_t err = OK;
+
+    if (n < 0) {
+        err = -errno;
+    } else if (n == 0) {
+        err = -ECONNRESET;
+    }
+
+    if (err != OK) {
+        notifyError(true /* send */, err, "Send failed.");
+        mSawSendFailure = true;
+    }
+
+#if 0
+    int numBytesQueued;
+    int res = ioctl(mSocket, SIOCOUTQ, &numBytesQueued);
+    if (res == 0 && numBytesQueued > 50 * 1024) {
+        if (numBytesQueued > 409600) {
+            ALOGW("!!! numBytesQueued = %d", numBytesQueued);
+        }
+
+        int64_t nowUs = ALooper::GetNowUs();
+
+        if (mLastStallReportUs < 0ll
+                || nowUs > mLastStallReportUs + 100000ll) {
+            sp<AMessage> msg = mNotify->dup();
+            msg->setInt32("sessionID", mSessionID);
+            msg->setInt32("reason", kWhatNetworkStall);
+            msg->setSize("numBytesQueued", numBytesQueued);
+            msg->post();
+
+            mLastStallReportUs = nowUs;
+        }
+    }
+#endif
+
+    return err;
+}
+
+status_t ANetworkSession::Session::sendRequest(
+        const void *data, ssize_t size, bool timeValid, int64_t timeUs) {
+    CHECK(mState == CONNECTED || mState == DATAGRAM);
+
+    if (size < 0) {
+        size = strlen((const char *)data);
+    }
+
+    if (size == 0) {
+        return OK;
+    }
+
+    sp<ABuffer> buffer;
+
+    if (mState == CONNECTED && mMode == MODE_DATAGRAM) {
+        CHECK_LE(size, 65535);
+
+        buffer = new ABuffer(size + 2);
+        buffer->data()[0] = size >> 8;
+        buffer->data()[1] = size & 0xff;
+        memcpy(buffer->data() + 2, data, size);
+    } else if (mState == CONNECTED && mMode == MODE_WEBSOCKET) {
+        static const bool kUseMask = false;  // Chromium doesn't like it.
+
+        size_t numHeaderBytes = 2 + (kUseMask ? 4 : 0);
+        if (size > 65535) {
+            numHeaderBytes += 8;
+        } else if (size > 125) {
+            numHeaderBytes += 2;
+        }
+
+        buffer = new ABuffer(numHeaderBytes + size);
+        buffer->data()[0] = 0x81;  // FIN==1 | opcode=1 (text)
+        buffer->data()[1] = kUseMask ? 0x80 : 0x00;
+
+        if (size > 65535) {
+            buffer->data()[1] |= 127;
+            buffer->data()[2] = 0x00;
+            buffer->data()[3] = 0x00;
+            buffer->data()[4] = 0x00;
+            buffer->data()[5] = 0x00;
+            buffer->data()[6] = (size >> 24) & 0xff;
+            buffer->data()[7] = (size >> 16) & 0xff;
+            buffer->data()[8] = (size >> 8) & 0xff;
+            buffer->data()[9] = size & 0xff;
+        } else if (size > 125) {
+            buffer->data()[1] |= 126;
+            buffer->data()[2] = (size >> 8) & 0xff;
+            buffer->data()[3] = size & 0xff;
+        } else {
+            buffer->data()[1] |= size;
+        }
+
+        if (kUseMask) {
+            uint32_t mask = rand();
+
+            buffer->data()[numHeaderBytes - 4] = (mask >> 24) & 0xff;
+            buffer->data()[numHeaderBytes - 3] = (mask >> 16) & 0xff;
+            buffer->data()[numHeaderBytes - 2] = (mask >> 8) & 0xff;
+            buffer->data()[numHeaderBytes - 1] = mask & 0xff;
+
+            for (size_t i = 0; i < (size_t)size; ++i) {
+                buffer->data()[numHeaderBytes + i] =
+                    ((const uint8_t *)data)[i]
+                        ^ ((mask >> (8 * (3 - (i % 4)))) & 0xff);
+            }
+        } else {
+            memcpy(buffer->data() + numHeaderBytes, data, size);
+        }
+    } else {
+        buffer = new ABuffer(size);
+        memcpy(buffer->data(), data, size);
+    }
+
+    Fragment frag;
+
+    frag.mFlags = 0;
+    if (timeValid) {
+        frag.mFlags = FRAGMENT_FLAG_TIME_VALID;
+        frag.mTimeUs = timeUs;
+    }
+
+    frag.mBuffer = buffer;
+
+    mOutFragments.push_back(frag);
+
+    return OK;
+}
+
+void ANetworkSession::Session::notifyError(
+        bool send, status_t err, const char *detail) {
+    sp<AMessage> msg = mNotify->dup();
+    msg->setInt32("sessionID", mSessionID);
+    msg->setInt32("reason", kWhatError);
+    msg->setInt32("send", send);
+    msg->setInt32("err", err);
+    msg->setString("detail", detail);
+    msg->post();
+}
+
+void ANetworkSession::Session::notify(NotificationReason reason) {
+    sp<AMessage> msg = mNotify->dup();
+    msg->setInt32("sessionID", mSessionID);
+    msg->setInt32("reason", reason);
+    msg->post();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ANetworkSession::ANetworkSession()
+    : mNextSessionID(1) {
+    mPipeFd[0] = mPipeFd[1] = -1;
+}
+
+ANetworkSession::~ANetworkSession() {
+    stop();
+}
+
+status_t ANetworkSession::start() {
+    if (mThread != NULL) {
+        return INVALID_OPERATION;
+    }
+
+    int res = pipe(mPipeFd);
+    if (res != 0) {
+        mPipeFd[0] = mPipeFd[1] = -1;
+        return -errno;
+    }
+
+    mThread = new NetworkThread(this);
+
+    status_t err = mThread->run("ANetworkSession");
+
+    if (err != OK) {
+        mThread.clear();
+
+        close(mPipeFd[0]);
+        close(mPipeFd[1]);
+        mPipeFd[0] = mPipeFd[1] = -1;
+
+        return err;
+    }
+
+    return OK;
+}
+
+status_t ANetworkSession::stop() {
+    if (mThread == NULL) {
+        return INVALID_OPERATION;
+    }
+
+    mThread->requestExit();
+    interrupt();
+    mThread->requestExitAndWait();
+
+    mThread.clear();
+
+    close(mPipeFd[0]);
+    close(mPipeFd[1]);
+    mPipeFd[0] = mPipeFd[1] = -1;
+
+    return OK;
+}
+
+status_t ANetworkSession::createRTSPClient(
+        const char *host, unsigned port, const sp<AMessage> &notify,
+        int32_t *sessionID) {
+    return createClientOrServer(
+            kModeCreateRTSPClient,
+            NULL /* addr */,
+            0 /* port */,
+            host,
+            port,
+            notify,
+            sessionID);
+}
+
+status_t ANetworkSession::createRTSPServer(
+        const struct in_addr &addr, unsigned port,
+        const sp<AMessage> &notify, int32_t *sessionID) {
+    return createClientOrServer(
+            kModeCreateRTSPServer,
+            &addr,
+            port,
+            NULL /* remoteHost */,
+            0 /* remotePort */,
+            notify,
+            sessionID);
+}
+
+status_t ANetworkSession::createUDPSession(
+        unsigned localPort, const sp<AMessage> &notify, int32_t *sessionID) {
+    return createUDPSession(localPort, NULL, 0, notify, sessionID);
+}
+
+status_t ANetworkSession::createUDPSession(
+        unsigned localPort,
+        const char *remoteHost,
+        unsigned remotePort,
+        const sp<AMessage> &notify,
+        int32_t *sessionID) {
+    return createClientOrServer(
+            kModeCreateUDPSession,
+            NULL /* addr */,
+            localPort,
+            remoteHost,
+            remotePort,
+            notify,
+            sessionID);
+}
+
+status_t ANetworkSession::createTCPDatagramSession(
+        const struct in_addr &addr, unsigned port,
+        const sp<AMessage> &notify, int32_t *sessionID) {
+    return createClientOrServer(
+            kModeCreateTCPDatagramSessionPassive,
+            &addr,
+            port,
+            NULL /* remoteHost */,
+            0 /* remotePort */,
+            notify,
+            sessionID);
+}
+
+status_t ANetworkSession::createTCPDatagramSession(
+        unsigned localPort,
+        const char *remoteHost,
+        unsigned remotePort,
+        const sp<AMessage> &notify,
+        int32_t *sessionID) {
+    return createClientOrServer(
+            kModeCreateTCPDatagramSessionActive,
+            NULL /* addr */,
+            localPort,
+            remoteHost,
+            remotePort,
+            notify,
+            sessionID);
+}
+
+status_t ANetworkSession::destroySession(int32_t sessionID) {
+    Mutex::Autolock autoLock(mLock);
+
+    auto it = mSessions.find(sessionID);
+
+    if (it == mSessions.end()) {
+        return -ENOENT;
+    }
+
+    mSessions.erase(it);
+
+    interrupt();
+
+    return OK;
+}
+
+// static
+status_t ANetworkSession::MakeSocketNonBlocking(int s) {
+    int flags = fcntl(s, F_GETFL, 0);
+    if (flags < 0) {
+        flags = 0;
+    }
+
+    int res = fcntl(s, F_SETFL, flags | O_NONBLOCK);
+    if (res < 0) {
+        return -errno;
+    }
+
+    return OK;
+}
+
+status_t ANetworkSession::createClientOrServer(
+        Mode mode,
+        const struct in_addr *localAddr,
+        unsigned port,
+        const char *remoteHost,
+        unsigned remotePort,
+        const sp<AMessage> &notify,
+        int32_t *sessionID) {
+    Mutex::Autolock autoLock(mLock);
+
+    *sessionID = 0;
+    status_t err = OK;
+    int s, res;
+    sp<Session> session;
+
+    s = socket(
+            AF_INET,
+            (mode == kModeCreateUDPSession) ? SOCK_DGRAM : SOCK_STREAM,
+            0);
+
+    if (s < 0) {
+        err = -errno;
+        goto bail;
+    }
+
+    if (mode == kModeCreateRTSPServer
+            || mode == kModeCreateTCPDatagramSessionPassive) {
+        const int yes = 1;
+        res = setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
+
+        if (res < 0) {
+            err = -errno;
+            goto bail2;
+        }
+    }
+
+    if (mode == kModeCreateUDPSession) {
+        int size = 256 * 1024;
+
+        res = setsockopt(s, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
+
+        if (res < 0) {
+            err = -errno;
+            goto bail2;
+        }
+
+        res = setsockopt(s, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size));
+
+        if (res < 0) {
+            err = -errno;
+            goto bail2;
+        }
+    } else if (mode == kModeCreateTCPDatagramSessionActive) {
+#if 0
+        int flag = 1;
+        res = setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
+
+        if (res < 0) {
+            err = -errno;
+            goto bail2;
+        }
+#endif
+
+        int tos = 224;  // VOICE
+        res = setsockopt(s, IPPROTO_IP, IP_TOS, &tos, sizeof(tos));
+
+        if (res < 0) {
+            err = -errno;
+            goto bail2;
+        }
+    }
+
+    err = MakeSocketNonBlocking(s);
+
+    if (err != OK) {
+        goto bail2;
+    }
+
+    struct sockaddr_in addr;
+    memset(addr.sin_zero, 0, sizeof(addr.sin_zero));
+    addr.sin_family = AF_INET;
+
+    if (mode == kModeCreateRTSPClient
+            || mode == kModeCreateTCPDatagramSessionActive) {
+        struct hostent *ent= gethostbyname(remoteHost);
+        if (ent == NULL) {
+            err = -h_errno;
+            goto bail2;
+        }
+
+        addr.sin_addr.s_addr = *(in_addr_t *)ent->h_addr;
+        addr.sin_port = htons(remotePort);
+    } else if (localAddr != NULL) {
+        addr.sin_addr = *localAddr;
+        addr.sin_port = htons(port);
+    } else {
+        addr.sin_addr.s_addr = htonl(INADDR_ANY);
+        addr.sin_port = htons(port);
+    }
+
+    if (mode == kModeCreateRTSPClient
+            || mode == kModeCreateTCPDatagramSessionActive) {
+        in_addr_t x = ntohl(addr.sin_addr.s_addr);
+        ALOGI("connecting socket %d to %d.%d.%d.%d:%d",
+              s,
+              (x >> 24),
+              (x >> 16) & 0xff,
+              (x >> 8) & 0xff,
+              x & 0xff,
+              ntohs(addr.sin_port));
+
+        res = connect(s, (const struct sockaddr *)&addr, sizeof(addr));
+
+        CHECK_LT(res, 0);
+        if (errno == EINPROGRESS) {
+            res = 0;
+        }
+    } else {
+        res = bind(s, (const struct sockaddr *)&addr, sizeof(addr));
+
+        if (res == 0) {
+            if (mode == kModeCreateRTSPServer
+                    || mode == kModeCreateTCPDatagramSessionPassive) {
+                res = listen(s, 4);
+            } else {
+                CHECK_EQ(mode, kModeCreateUDPSession);
+
+                if (remoteHost != NULL) {
+                    struct sockaddr_in remoteAddr;
+                    memset(remoteAddr.sin_zero, 0, sizeof(remoteAddr.sin_zero));
+                    remoteAddr.sin_family = AF_INET;
+                    remoteAddr.sin_port = htons(remotePort);
+
+                    struct hostent *ent= gethostbyname(remoteHost);
+                    if (ent == NULL) {
+                        err = -h_errno;
+                        goto bail2;
+                    }
+
+                    remoteAddr.sin_addr.s_addr = *(in_addr_t *)ent->h_addr;
+
+                    res = connect(
+                            s,
+                            (const struct sockaddr *)&remoteAddr,
+                            sizeof(remoteAddr));
+                }
+            }
+        }
+    }
+
+    if (res < 0) {
+        err = -errno;
+        goto bail2;
+    }
+
+    Session::State state;
+    switch (mode) {
+        case kModeCreateRTSPClient:
+            state = Session::CONNECTING;
+            break;
+
+        case kModeCreateTCPDatagramSessionActive:
+            state = Session::CONNECTING;
+            break;
+
+        case kModeCreateTCPDatagramSessionPassive:
+            state = Session::LISTENING_TCP_DGRAMS;
+            break;
+
+        case kModeCreateRTSPServer:
+            state = Session::LISTENING_RTSP;
+            break;
+
+        default:
+            CHECK_EQ(mode, kModeCreateUDPSession);
+            state = Session::DATAGRAM;
+            break;
+    }
+
+    session = new Session(
+            mNextSessionID++,
+            state,
+            s,
+            notify);
+
+    if (mode == kModeCreateTCPDatagramSessionActive) {
+        session->setMode(Session::MODE_DATAGRAM);
+    } else if (mode == kModeCreateRTSPClient) {
+        session->setMode(Session::MODE_RTSP);
+    }
+
+    mSessions[session->sessionID()] = session;
+
+    interrupt();
+
+    *sessionID = session->sessionID();
+
+    goto bail;
+
+bail2:
+    close(s);
+    s = -1;
+
+bail:
+    return err;
+}
+
+status_t ANetworkSession::connectUDPSession(
+        int32_t sessionID, const char *remoteHost, unsigned remotePort) {
+    Mutex::Autolock autoLock(mLock);
+
+    auto it = mSessions.find(sessionID);
+
+    if (it == mSessions.end()) {
+        return -ENOENT;
+    }
+
+    const sp<Session> session = it->second;
+    int s = session->socket();
+
+    struct sockaddr_in remoteAddr;
+    memset(remoteAddr.sin_zero, 0, sizeof(remoteAddr.sin_zero));
+    remoteAddr.sin_family = AF_INET;
+    remoteAddr.sin_port = htons(remotePort);
+
+    status_t err = OK;
+    struct hostent *ent = gethostbyname(remoteHost);
+    if (ent == NULL) {
+        err = -h_errno;
+    } else {
+        remoteAddr.sin_addr.s_addr = *(in_addr_t *)ent->h_addr;
+
+        int res = connect(
+                s,
+                (const struct sockaddr *)&remoteAddr,
+                sizeof(remoteAddr));
+
+        if (res < 0) {
+            err = -errno;
+        }
+    }
+
+    return err;
+}
+
+status_t ANetworkSession::sendRequest(
+        int32_t sessionID, const void *data, ssize_t size,
+        bool timeValid, int64_t timeUs) {
+    Mutex::Autolock autoLock(mLock);
+
+    auto it = mSessions.find(sessionID);
+
+    if (it == mSessions.end()) {
+        return -ENOENT;
+    }
+
+    const sp<Session> session = it->second;
+
+    status_t err = session->sendRequest(data, size, timeValid, timeUs);
+
+    interrupt();
+
+    return err;
+}
+
+status_t ANetworkSession::switchToWebSocketMode(int32_t sessionID) {
+    Mutex::Autolock autoLock(mLock);
+
+    auto it = mSessions.find(sessionID);
+
+    if (it == mSessions.end()) {
+        return -ENOENT;
+    }
+
+    const sp<Session> session = it->second;
+    return session->switchToWebSocketMode();
+}
+
+void ANetworkSession::interrupt() {
+    static const char dummy = 0;
+
+    ssize_t n;
+    do {
+        n = write(mPipeFd[1], &dummy, 1);
+    } while (n < 0 && errno == EINTR);
+
+    if (n < 0) {
+        ALOGW("Error writing to pipe (%s)", strerror(errno));
+    }
+}
+
+void ANetworkSession::threadLoop() {
+    fd_set rs, ws;
+    FD_ZERO(&rs);
+    FD_ZERO(&ws);
+
+    FD_SET(mPipeFd[0], &rs);
+    int maxFd = mPipeFd[0];
+
+    {
+        Mutex::Autolock autoLock(mLock);
+
+        for (const auto &pair : mSessions) {
+            const sp<Session> &session = pair.second;
+
+            int s = session->socket();
+
+            if (s < 0) {
+                continue;
+            }
+
+            if (session->wantsToRead()) {
+                FD_SET(s, &rs);
+                if (s > maxFd) {
+                    maxFd = s;
+                }
+            }
+
+            if (session->wantsToWrite()) {
+                FD_SET(s, &ws);
+                if (s > maxFd) {
+                    maxFd = s;
+                }
+            }
+        }
+    }
+
+    int res = select(maxFd + 1, &rs, &ws, NULL, NULL /* tv */);
+
+    if (res == 0) {
+        return;
+    }
+
+    if (res < 0) {
+        if (errno == EINTR) {
+            return;
+        }
+
+        ALOGE("select failed w/ error %d (%s)", errno, strerror(errno));
+        return;
+    }
+
+    if (FD_ISSET(mPipeFd[0], &rs)) {
+        char c;
+        ssize_t n;
+        do {
+            n = read(mPipeFd[0], &c, 1);
+        } while (n < 0 && errno == EINTR);
+
+        if (n < 0) {
+            ALOGW("Error reading from pipe (%s)", strerror(errno));
+        }
+
+        --res;
+    }
+
+    {
+        Mutex::Autolock autoLock(mLock);
+
+        std::list<sp<Session>> sessionsToAdd;
+
+        for (const auto &pair : mSessions) {
+            if (res <= 0) {
+                break;
+            }
+
+            const sp<Session> &session = pair.second;
+
+            int s = session->socket();
+
+            if (s < 0) {
+                continue;
+            }
+
+            if (FD_ISSET(s, &rs) || FD_ISSET(s, &ws)) {
+                --res;
+            }
+
+            if (FD_ISSET(s, &rs)) {
+                if (session->isRTSPServer() || session->isTCPDatagramServer()) {
+                    struct sockaddr_in remoteAddr;
+                    socklen_t remoteAddrLen = sizeof(remoteAddr);
+
+                    int clientSocket = accept(
+                            s, (struct sockaddr *)&remoteAddr, &remoteAddrLen);
+
+                    if (clientSocket >= 0) {
+                        status_t err = MakeSocketNonBlocking(clientSocket);
+
+                        if (err != OK) {
+                            ALOGE("Unable to make client socket non blocking, "
+                                  "failed w/ error %d (%s)",
+                                  err, strerror(-err));
+
+                            close(clientSocket);
+                            clientSocket = -1;
+                        } else {
+                            in_addr_t addr = ntohl(remoteAddr.sin_addr.s_addr);
+
+                            ALOGI("incoming connection from %d.%d.%d.%d:%d "
+                                  "(socket %d)",
+                                  (addr >> 24),
+                                  (addr >> 16) & 0xff,
+                                  (addr >> 8) & 0xff,
+                                  addr & 0xff,
+                                  ntohs(remoteAddr.sin_port),
+                                  clientSocket);
+
+                            sp<Session> clientSession =
+                                new Session(
+                                        mNextSessionID++,
+                                        Session::CONNECTED,
+                                        clientSocket,
+                                        session->getNotificationMessage());
+
+                            clientSession->setMode(
+                                    session->isRTSPServer()
+                                        ? Session::MODE_RTSP
+                                        : Session::MODE_DATAGRAM);
+
+                            sessionsToAdd.push_back(clientSession);
+                        }
+                    } else {
+                        ALOGE("accept returned error %d (%s)",
+                              errno, strerror(errno));
+                    }
+                } else {
+                    status_t err = session->readMore();
+                    if (err != OK) {
+                        ALOGE("readMore on socket %d failed w/ error %d (%s)",
+                              s, err, strerror(-err));
+                    }
+                }
+            }
+
+            if (FD_ISSET(s, &ws)) {
+                status_t err = session->writeMore();
+                if (err != OK) {
+                    ALOGE("writeMore on socket %d failed w/ error %d (%s)",
+                          s, err, strerror(-err));
+                }
+            }
+        }
+
+        while (!sessionsToAdd.empty()) {
+            sp<Session> session = *sessionsToAdd.begin();
+            sessionsToAdd.erase(sessionsToAdd.begin());
+
+            mSessions[session->sessionID()] = session;
+
+            ALOGI("added clientSession %d", session->sessionID());
+        }
+    }
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/ATSParser.cpp b/host/frontend/gcastv2/libandroid/ATSParser.cpp
new file mode 100644
index 0000000..2275f5d
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/ATSParser.cpp
@@ -0,0 +1,1308 @@
+/*
+ * 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.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "ATSParser"
+#include <utils/Log.h>
+
+#include <media/stagefright/ATSParser.h>
+
+#include "ESQueue.h"
+
+#include <media/stagefright/avc_utils.h>
+#include <media/stagefright/foundation/ABitReader.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/AnotherPacketSource.h>
+#include <media/stagefright/MediaDefs.h>
+#include <media/stagefright/MediaErrors.h>
+#include <media/stagefright/MetaData.h>
+#include <media/stagefright/Utils.h>
+
+#include <map>
+#include <vector>
+
+namespace android {
+
+// I want the expression "y" evaluated even if verbose logging is off.
+#define MY_LOGV(x, y) \
+    do { unsigned tmp = y; ALOGV(x, tmp); tmp = tmp; } while (0)
+
+static const size_t kTSPacketSize = 188;
+
+struct ATSParser::Program : public RefBase {
+    Program(ATSParser *parser, unsigned programNumber, unsigned programMapPID);
+
+    bool parsePSISection(
+            unsigned pid, ABitReader *br, status_t *err);
+
+    bool parsePID(
+            unsigned pid, unsigned continuity_counter,
+            unsigned payload_unit_start_indicator,
+            ABitReader *br, status_t *err);
+
+    void signalDiscontinuity(
+            DiscontinuityType type, const sp<AMessage> &extra);
+
+    void signalEOS(status_t finalResult);
+
+    sp<AnotherPacketSource> getSource(SourceType type);
+
+    int64_t convertPTSToTimestamp(uint64_t PTS);
+
+    bool PTSTimeDeltaEstablished() const {
+        return mFirstPTSValid;
+    }
+
+    unsigned number() const { return mProgramNumber; }
+
+    void updateProgramMapPID(unsigned programMapPID) {
+        mProgramMapPID = programMapPID;
+    }
+
+    unsigned programMapPID() const {
+        return mProgramMapPID;
+    }
+
+    uint32_t parserFlags() const {
+        return mParser->mFlags;
+    }
+
+private:
+    ATSParser *mParser;
+    unsigned mProgramNumber;
+    unsigned mProgramMapPID;
+    std::map<unsigned, sp<Stream>> mStreams;
+    bool mFirstPTSValid;
+    uint64_t mFirstPTS;
+
+    status_t parseProgramMap(ABitReader *br);
+
+    DISALLOW_EVIL_CONSTRUCTORS(Program);
+};
+
+struct ATSParser::Stream : public RefBase {
+    Stream(Program *program,
+           unsigned elementaryPID,
+           unsigned streamType,
+           unsigned PCR_PID);
+
+    unsigned type() const { return mStreamType; }
+    unsigned pid() const { return mElementaryPID; }
+    void setPID(unsigned pid) { mElementaryPID = pid; }
+
+    status_t parse(
+            unsigned continuity_counter,
+            unsigned payload_unit_start_indicator,
+            ABitReader *br);
+
+    void signalDiscontinuity(
+            DiscontinuityType type, const sp<AMessage> &extra);
+
+    void signalEOS(status_t finalResult);
+
+    sp<AnotherPacketSource> getSource(SourceType type);
+
+protected:
+    virtual ~Stream();
+
+private:
+    Program *mProgram;
+    unsigned mElementaryPID;
+    unsigned mStreamType;
+    unsigned mPCR_PID;
+    int32_t mExpectedContinuityCounter;
+
+    sp<ABuffer> mBuffer;
+    sp<AnotherPacketSource> mSource;
+    bool mPayloadStarted;
+
+    uint64_t mPrevPTS;
+
+    ElementaryStreamQueue *mQueue;
+
+    status_t flush();
+    status_t parsePES(ABitReader *br);
+
+    void onPayloadData(
+            unsigned PTS_DTS_flags, uint64_t PTS, uint64_t DTS,
+            const uint8_t *data, size_t size);
+
+    void extractAACFrames(const sp<ABuffer> &buffer);
+
+    bool isAudio() const;
+    bool isVideo() const;
+
+    DISALLOW_EVIL_CONSTRUCTORS(Stream);
+};
+
+struct ATSParser::PSISection : public RefBase {
+    PSISection();
+
+    status_t append(const void *data, size_t size);
+    void clear();
+
+    bool isComplete() const;
+    bool isEmpty() const;
+
+    const uint8_t *data() const;
+    size_t size() const;
+
+protected:
+    virtual ~PSISection();
+
+private:
+    sp<ABuffer> mBuffer;
+
+    DISALLOW_EVIL_CONSTRUCTORS(PSISection);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+ATSParser::Program::Program(
+        ATSParser *parser, unsigned programNumber, unsigned programMapPID)
+    : mParser(parser),
+      mProgramNumber(programNumber),
+      mProgramMapPID(programMapPID),
+      mFirstPTSValid(false),
+      mFirstPTS(0) {
+    ALOGV("new program number %u", programNumber);
+}
+
+bool ATSParser::Program::parsePSISection(
+        unsigned pid, ABitReader *br, status_t *err) {
+    *err = OK;
+
+    if (pid != mProgramMapPID) {
+        return false;
+    }
+
+    *err = parseProgramMap(br);
+
+    return true;
+}
+
+bool ATSParser::Program::parsePID(
+        unsigned pid, unsigned continuity_counter,
+        unsigned payload_unit_start_indicator,
+        ABitReader *br, status_t *err) {
+    *err = OK;
+
+    auto it = mStreams.find(pid);
+    if (it == mStreams.end()) {
+        return false;
+    }
+
+    *err = it->second->parse(
+            continuity_counter, payload_unit_start_indicator, br);
+
+    return true;
+}
+
+void ATSParser::Program::signalDiscontinuity(
+        DiscontinuityType type, const sp<AMessage> &extra) {
+    for (const auto &pair : mStreams) {
+        pair.second->signalDiscontinuity(type, extra);
+    }
+}
+
+void ATSParser::Program::signalEOS(status_t finalResult) {
+    for (const auto &pair : mStreams) {
+        pair.second->signalEOS(finalResult);
+    }
+}
+
+struct StreamInfo {
+    unsigned mType;
+    unsigned mPID;
+};
+
+status_t ATSParser::Program::parseProgramMap(ABitReader *br) {
+    unsigned table_id = br->getBits(8);
+    ALOGV("  table_id = %u", table_id);
+    CHECK_EQ(table_id, 0x02u);
+
+    unsigned section_syntax_indicator = br->getBits(1);
+    ALOGV("  section_syntax_indicator = %u", section_syntax_indicator);
+    CHECK_EQ(section_syntax_indicator, 1u);
+
+    CHECK_EQ(br->getBits(1), 0u);
+    MY_LOGV("  reserved = %u", br->getBits(2));
+
+    unsigned section_length = br->getBits(12);
+    ALOGV("  section_length = %u", section_length);
+    CHECK_EQ(section_length & 0xc00, 0u);
+    CHECK_LE(section_length, 1021u);
+
+    MY_LOGV("  program_number = %u", br->getBits(16));
+    MY_LOGV("  reserved = %u", br->getBits(2));
+    MY_LOGV("  version_number = %u", br->getBits(5));
+    MY_LOGV("  current_next_indicator = %u", br->getBits(1));
+    MY_LOGV("  section_number = %u", br->getBits(8));
+    MY_LOGV("  last_section_number = %u", br->getBits(8));
+    MY_LOGV("  reserved = %u", br->getBits(3));
+
+    unsigned PCR_PID = br->getBits(13);
+    ALOGV("  PCR_PID = 0x%04x", PCR_PID);
+
+    MY_LOGV("  reserved = %u", br->getBits(4));
+
+    unsigned program_info_length = br->getBits(12);
+    ALOGV("  program_info_length = %u", program_info_length);
+    CHECK_EQ(program_info_length & 0xc00, 0u);
+
+    br->skipBits(program_info_length * 8);  // skip descriptors
+
+    std::vector<StreamInfo> infos;
+
+    // infoBytesRemaining is the number of bytes that make up the
+    // variable length section of ES_infos. It does not include the
+    // final CRC.
+    size_t infoBytesRemaining = section_length - 9 - program_info_length - 4;
+
+    while (infoBytesRemaining > 0) {
+        CHECK_GE(infoBytesRemaining, 5u);
+
+        unsigned streamType = br->getBits(8);
+        ALOGV("    stream_type = 0x%02x", streamType);
+
+        MY_LOGV("    reserved = %u", br->getBits(3));
+
+        unsigned elementaryPID = br->getBits(13);
+        ALOGV("    elementary_PID = 0x%04x", elementaryPID);
+
+        MY_LOGV("    reserved = %u", br->getBits(4));
+
+        unsigned ES_info_length = br->getBits(12);
+        ALOGV("    ES_info_length = %u", ES_info_length);
+        CHECK_EQ(ES_info_length & 0xc00, 0u);
+
+        CHECK_GE(infoBytesRemaining - 5, ES_info_length);
+
+#if 0
+        br->skipBits(ES_info_length * 8);  // skip descriptors
+#else
+        unsigned info_bytes_remaining = ES_info_length;
+        while (info_bytes_remaining >= 2) {
+            MY_LOGV("      tag = 0x%02x", br->getBits(8));
+
+            unsigned descLength = br->getBits(8);
+            ALOGV("      len = %u", descLength);
+
+            CHECK_GE(info_bytes_remaining, 2 + descLength);
+
+            br->skipBits(descLength * 8);
+
+            info_bytes_remaining -= descLength + 2;
+        }
+        CHECK_EQ(info_bytes_remaining, 0u);
+#endif
+
+        StreamInfo info;
+        info.mType = streamType;
+        info.mPID = elementaryPID;
+        infos.push_back(info);
+
+        infoBytesRemaining -= 5 + ES_info_length;
+    }
+
+    CHECK_EQ(infoBytesRemaining, 0u);
+    MY_LOGV("  CRC = 0x%08x", br->getBits(32));
+
+    bool PIDsChanged = false;
+    for (const auto &info : infos) {
+        auto it = mStreams.find(info.mPID);
+        if (it != mStreams.end() && it->second->type() != info.mType) {
+            ALOGI("%s", "uh oh. stream PIDs have changed.");
+            PIDsChanged = true;
+            break;
+        }
+    }
+
+    if (PIDsChanged) {
+#if 0
+        ALOGI("before:");
+        for (size_t i = 0; i < mStreams.size(); ++i) {
+            sp<Stream> stream = mStreams.editValueAt(i);
+
+            ALOGI("PID 0x%08x => type 0x%02x", stream->pid(), stream->type());
+        }
+
+        ALOGI("after:");
+        for (size_t i = 0; i < infos.size(); ++i) {
+            StreamInfo &info = infos.editItemAt(i);
+
+            ALOGI("PID 0x%08x => type 0x%02x", info.mPID, info.mType);
+        }
+#endif
+
+        // The only case we can recover from is if we have two streams
+        // and they switched PIDs.
+
+        bool success = false;
+
+        if (mStreams.size() == 2 && infos.size() == 2) {
+            const StreamInfo &info1 = infos.at(0);
+            const StreamInfo &info2 = infos.at(1);
+
+            sp<Stream> s1 = mStreams.begin()->second;
+            sp<Stream> s2 = (++mStreams.begin())->second;
+
+            bool caseA =
+                info1.mPID == s1->pid() && info1.mType == s2->type()
+                    && info2.mPID == s2->pid() && info2.mType == s1->type();
+
+            bool caseB =
+                info1.mPID == s2->pid() && info1.mType == s1->type()
+                    && info2.mPID == s1->pid() && info2.mType == s2->type();
+
+            if (caseA || caseB) {
+                unsigned pid1 = s1->pid();
+                unsigned pid2 = s2->pid();
+                s1->setPID(pid2);
+                s2->setPID(pid1);
+
+                mStreams.clear();
+                mStreams[s1->pid()] = s1;
+                mStreams[s2->pid()] = s2;
+
+                success = true;
+            }
+        }
+
+        if (!success) {
+            ALOGI("%s", "Stream PIDs changed and we cannot recover.");
+            return ERROR_MALFORMED;
+        }
+    }
+
+    for (const StreamInfo &info : infos) {
+        auto it = mStreams.find(info.mPID);
+        if (it == mStreams.end()) {
+            sp<Stream> stream = new Stream(
+                    this, info.mPID, info.mType, PCR_PID);
+
+            mStreams[info.mPID] = stream;
+        }
+    }
+
+    return OK;
+}
+
+sp<AnotherPacketSource> ATSParser::Program::getSource(SourceType type) {
+    size_t index = (type == AUDIO) ? 0 : 0;
+
+    for (const auto &pair : mStreams) {
+        sp<AnotherPacketSource> source = pair.second->getSource(type);
+        if (source != NULL) {
+            if (index == 0) {
+                return source;
+            }
+            --index;
+        }
+    }
+
+    return NULL;
+}
+
+int64_t ATSParser::Program::convertPTSToTimestamp(uint64_t PTS) {
+    if (mParser->mFlags & DUMP_PTS) {
+        ALOGI("PTS = 0x%016llx", PTS);
+    }
+
+    if (!(mParser->mFlags & TS_TIMESTAMPS_ARE_ABSOLUTE)) {
+        if (!mFirstPTSValid) {
+            mFirstPTSValid = true;
+            mFirstPTS = PTS;
+            PTS = 0;
+        } else if (PTS < mFirstPTS) {
+            PTS = 0;
+        } else {
+            PTS -= mFirstPTS;
+        }
+    }
+
+    int64_t timeUs = (PTS * 100) / 9;
+
+    if (mParser->mAbsoluteTimeAnchorUs >= 0ll) {
+        timeUs += mParser->mAbsoluteTimeAnchorUs;
+    }
+
+    if (mParser->mTimeOffsetValid) {
+        timeUs += mParser->mTimeOffsetUs;
+    }
+
+    return timeUs;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ATSParser::Stream::Stream(
+        Program *program,
+        unsigned elementaryPID,
+        unsigned streamType,
+        unsigned PCR_PID)
+    : mProgram(program),
+      mElementaryPID(elementaryPID),
+      mStreamType(streamType),
+      mPCR_PID(PCR_PID),
+      mExpectedContinuityCounter(-1),
+      mPayloadStarted(false),
+      mPrevPTS(0),
+      mQueue(NULL) {
+    switch (mStreamType) {
+        case STREAMTYPE_H264:
+            mQueue = new ElementaryStreamQueue(
+                    ElementaryStreamQueue::H264,
+                    (mProgram->parserFlags() & ALIGNED_VIDEO_DATA)
+                        ? ElementaryStreamQueue::kFlag_AlignedData : 0);
+            break;
+        case STREAMTYPE_MPEG2_AUDIO_ADTS:
+            mQueue = new ElementaryStreamQueue(ElementaryStreamQueue::AAC);
+            break;
+        case STREAMTYPE_MPEG1_AUDIO:
+        case STREAMTYPE_MPEG2_AUDIO:
+            mQueue = new ElementaryStreamQueue(
+                    ElementaryStreamQueue::MPEG_AUDIO);
+            break;
+
+        case STREAMTYPE_MPEG1_VIDEO:
+        case STREAMTYPE_MPEG2_VIDEO:
+            mQueue = new ElementaryStreamQueue(
+                    ElementaryStreamQueue::MPEG_VIDEO);
+            break;
+
+        case STREAMTYPE_MPEG4_VIDEO:
+            mQueue = new ElementaryStreamQueue(
+                    ElementaryStreamQueue::MPEG4_VIDEO);
+            break;
+
+        case STREAMTYPE_PCM_AUDIO:
+            mQueue = new ElementaryStreamQueue(
+                    ElementaryStreamQueue::PCM_AUDIO);
+            break;
+
+        default:
+            break;
+    }
+
+    ALOGV("new stream PID 0x%02x, type 0x%02x", elementaryPID, streamType);
+
+    if (mQueue != NULL) {
+        mBuffer = new ABuffer(192 * 1024);
+        mBuffer->setRange(0, 0);
+    }
+}
+
+ATSParser::Stream::~Stream() {
+    delete mQueue;
+    mQueue = NULL;
+}
+
+status_t ATSParser::Stream::parse(
+        unsigned continuity_counter,
+        unsigned payload_unit_start_indicator, ABitReader *br) {
+    if (mQueue == NULL) {
+        return OK;
+    }
+
+    if (mExpectedContinuityCounter >= 0
+            && (unsigned)mExpectedContinuityCounter != continuity_counter) {
+        ALOGI("discontinuity on stream pid 0x%04x", mElementaryPID);
+
+        mPayloadStarted = false;
+        mBuffer->setRange(0, 0);
+        mExpectedContinuityCounter = -1;
+
+#if 0
+        // Uncomment this if you'd rather see no corruption whatsoever on
+        // screen and suspend updates until we come across another IDR frame.
+
+        if (mStreamType == STREAMTYPE_H264) {
+            ALOGI("clearing video queue");
+            mQueue->clear(true /* clearFormat */);
+        }
+#endif
+
+        return OK;
+    }
+
+    mExpectedContinuityCounter = (continuity_counter + 1) & 0x0f;
+
+    if (payload_unit_start_indicator) {
+        if (mPayloadStarted) {
+            // Otherwise we run the danger of receiving the trailing bytes
+            // of a PES packet that we never saw the start of and assuming
+            // we have a a complete PES packet.
+
+            status_t err = flush();
+
+            if (err != OK) {
+                return err;
+            }
+        }
+
+        mPayloadStarted = true;
+    }
+
+    if (!mPayloadStarted) {
+        return OK;
+    }
+
+    size_t payloadSizeBits = br->numBitsLeft();
+    CHECK_EQ(payloadSizeBits % 8, 0u);
+
+    size_t neededSize = mBuffer->size() + payloadSizeBits / 8;
+    if (mBuffer->capacity() < neededSize) {
+        // Increment in multiples of 64K.
+        neededSize = (neededSize + 65535) & ~65535;
+
+        ALOGI("resizing buffer to %ld bytes", neededSize);
+
+        sp<ABuffer> newBuffer = new ABuffer(neededSize);
+        memcpy(newBuffer->data(), mBuffer->data(), mBuffer->size());
+        newBuffer->setRange(0, mBuffer->size());
+        mBuffer = newBuffer;
+    }
+
+    memcpy(mBuffer->data() + mBuffer->size(), br->data(), payloadSizeBits / 8);
+    mBuffer->setRange(0, mBuffer->size() + payloadSizeBits / 8);
+
+    return OK;
+}
+
+bool ATSParser::Stream::isVideo() const {
+    switch (mStreamType) {
+        case STREAMTYPE_H264:
+        case STREAMTYPE_MPEG1_VIDEO:
+        case STREAMTYPE_MPEG2_VIDEO:
+        case STREAMTYPE_MPEG4_VIDEO:
+            return true;
+
+        default:
+            return false;
+    }
+}
+
+bool ATSParser::Stream::isAudio() const {
+    switch (mStreamType) {
+        case STREAMTYPE_MPEG1_AUDIO:
+        case STREAMTYPE_MPEG2_AUDIO:
+        case STREAMTYPE_MPEG2_AUDIO_ADTS:
+        case STREAMTYPE_PCM_AUDIO:
+            return true;
+
+        default:
+            return false;
+    }
+}
+
+void ATSParser::Stream::signalDiscontinuity(
+        DiscontinuityType type, const sp<AMessage> &extra) {
+    mExpectedContinuityCounter = -1;
+
+    if (mQueue == NULL) {
+        return;
+    }
+
+    mPayloadStarted = false;
+    mBuffer->setRange(0, 0);
+
+    bool clearFormat = false;
+    if (isAudio()) {
+        if (type & DISCONTINUITY_AUDIO_FORMAT) {
+            clearFormat = true;
+        }
+    } else {
+        if (type & DISCONTINUITY_VIDEO_FORMAT) {
+            clearFormat = true;
+        }
+    }
+
+    mQueue->clear(clearFormat);
+
+    if (mSource != NULL) {
+        mSource->queueDiscontinuity(type, extra);
+    }
+}
+
+void ATSParser::Stream::signalEOS(status_t finalResult) {
+    if (mSource != NULL) {
+        mSource->signalEOS(finalResult);
+    }
+}
+
+status_t ATSParser::Stream::parsePES(ABitReader *br) {
+    unsigned packet_startcode_prefix = br->getBits(24);
+
+    ALOGV("packet_startcode_prefix = 0x%08x", packet_startcode_prefix);
+
+    if (packet_startcode_prefix != 1) {
+        ALOGV("Supposedly payload_unit_start=1 unit does not start "
+             "with startcode.");
+
+        return ERROR_MALFORMED;
+    }
+
+    CHECK_EQ(packet_startcode_prefix, 0x000001u);
+
+    unsigned stream_id = br->getBits(8);
+    ALOGV("stream_id = 0x%02x", stream_id);
+
+    unsigned PES_packet_length = br->getBits(16);
+    ALOGV("PES_packet_length = %u", PES_packet_length);
+
+    if (stream_id != 0xbc  // program_stream_map
+            && stream_id != 0xbe  // padding_stream
+            && stream_id != 0xbf  // private_stream_2
+            && stream_id != 0xf0  // ECM
+            && stream_id != 0xf1  // EMM
+            && stream_id != 0xff  // program_stream_directory
+            && stream_id != 0xf2  // DSMCC
+            && stream_id != 0xf8) {  // H.222.1 type E
+        CHECK_EQ(br->getBits(2), 2u);
+
+        MY_LOGV("PES_scrambling_control = %u", br->getBits(2));
+        MY_LOGV("PES_priority = %u", br->getBits(1));
+        MY_LOGV("data_alignment_indicator = %u", br->getBits(1));
+        MY_LOGV("copyright = %u", br->getBits(1));
+        MY_LOGV("original_or_copy = %u", br->getBits(1));
+
+        unsigned PTS_DTS_flags = br->getBits(2);
+        ALOGV("PTS_DTS_flags = %u", PTS_DTS_flags);
+
+        unsigned ESCR_flag = br->getBits(1);
+        ALOGV("ESCR_flag = %u", ESCR_flag);
+
+        unsigned ES_rate_flag = br->getBits(1);
+        ALOGV("ES_rate_flag = %u", ES_rate_flag);
+
+        MY_LOGV("DSM_trick_mode_flag = %u", br->getBits(1));
+        MY_LOGV("additional_copy_info_flag = %u", br->getBits(1));
+        MY_LOGV("PES_CRC_flag = %u", br->getBits(1));
+        MY_LOGV("PES_extension_flag = %u", br->getBits(1));
+
+        unsigned PES_header_data_length = br->getBits(8);
+        ALOGV("PES_header_data_length = %u", PES_header_data_length);
+
+        unsigned optional_bytes_remaining = PES_header_data_length;
+
+        uint64_t PTS = 0, DTS = 0;
+
+        if (PTS_DTS_flags == 2 || PTS_DTS_flags == 3) {
+            CHECK_GE(optional_bytes_remaining, 5u);
+
+            CHECK_EQ(br->getBits(4), PTS_DTS_flags);
+
+            PTS = ((uint64_t)br->getBits(3)) << 30;
+            CHECK_EQ(br->getBits(1), 1u);
+            PTS |= ((uint64_t)br->getBits(15)) << 15;
+            CHECK_EQ(br->getBits(1), 1u);
+            PTS |= br->getBits(15);
+            CHECK_EQ(br->getBits(1), 1u);
+
+            ALOGV("PTS = 0x%016llx (%.2f)", PTS, PTS / 90000.0);
+
+            optional_bytes_remaining -= 5;
+
+            if (PTS_DTS_flags == 3) {
+                CHECK_GE(optional_bytes_remaining, 5u);
+
+                CHECK_EQ(br->getBits(4), 1u);
+
+                DTS = ((uint64_t)br->getBits(3)) << 30;
+                CHECK_EQ(br->getBits(1), 1u);
+                DTS |= ((uint64_t)br->getBits(15)) << 15;
+                CHECK_EQ(br->getBits(1), 1u);
+                DTS |= br->getBits(15);
+                CHECK_EQ(br->getBits(1), 1u);
+
+                ALOGV("DTS = %llu", DTS);
+
+                optional_bytes_remaining -= 5;
+            }
+        }
+
+        if (ESCR_flag) {
+            CHECK_GE(optional_bytes_remaining, 6u);
+
+            br->getBits(2);
+
+            uint64_t ESCR = ((uint64_t)br->getBits(3)) << 30;
+            CHECK_EQ(br->getBits(1), 1u);
+            ESCR |= ((uint64_t)br->getBits(15)) << 15;
+            CHECK_EQ(br->getBits(1), 1u);
+            ESCR |= br->getBits(15);
+            CHECK_EQ(br->getBits(1), 1u);
+
+            ALOGV("ESCR = %llu", ESCR);
+            MY_LOGV("ESCR_extension = %u", br->getBits(9));
+
+            CHECK_EQ(br->getBits(1), 1u);
+
+            optional_bytes_remaining -= 6;
+        }
+
+        if (ES_rate_flag) {
+            CHECK_GE(optional_bytes_remaining, 3u);
+
+            CHECK_EQ(br->getBits(1), 1u);
+            MY_LOGV("ES_rate = %u", br->getBits(22));
+            CHECK_EQ(br->getBits(1), 1u);
+
+            optional_bytes_remaining -= 3;
+        }
+
+        br->skipBits(optional_bytes_remaining * 8);
+
+        // ES data follows.
+
+        if (PES_packet_length != 0) {
+            CHECK_GE(PES_packet_length, PES_header_data_length + 3);
+
+            unsigned dataLength =
+                PES_packet_length - 3 - PES_header_data_length;
+
+            if (br->numBitsLeft() < dataLength * 8) {
+                ALOGE("PES packet does not carry enough data to contain "
+                     "payload. (numBitsLeft = %ld, required = %d)",
+                     br->numBitsLeft(), dataLength * 8);
+
+                return ERROR_MALFORMED;
+            }
+
+            CHECK_GE(br->numBitsLeft(), dataLength * 8);
+
+            onPayloadData(
+                    PTS_DTS_flags, PTS, DTS, br->data(), dataLength);
+
+            br->skipBits(dataLength * 8);
+        } else {
+            onPayloadData(
+                    PTS_DTS_flags, PTS, DTS,
+                    br->data(), br->numBitsLeft() / 8);
+
+            size_t payloadSizeBits = br->numBitsLeft();
+            CHECK_EQ(payloadSizeBits % 8, 0u);
+
+            ALOGV("There's %d bytes of payload.", payloadSizeBits / 8);
+        }
+    } else if (stream_id == 0xbe) {  // padding_stream
+        CHECK_NE(PES_packet_length, 0u);
+        br->skipBits(PES_packet_length * 8);
+    } else {
+        CHECK_NE(PES_packet_length, 0u);
+        br->skipBits(PES_packet_length * 8);
+    }
+
+    return OK;
+}
+
+status_t ATSParser::Stream::flush() {
+    if (mBuffer->size() == 0) {
+        return OK;
+    }
+
+    ALOGV("flushing stream 0x%04x size = %d", mElementaryPID, mBuffer->size());
+
+    ABitReader br(mBuffer->data(), mBuffer->size());
+
+    status_t err = parsePES(&br);
+
+    mBuffer->setRange(0, 0);
+
+    return err;
+}
+
+void ATSParser::Stream::onPayloadData(
+        unsigned PTS_DTS_flags, uint64_t PTS, uint64_t /* DTS */,
+        const uint8_t *data, size_t size) {
+#if 0
+    ALOGI("payload streamType 0x%02x, PTS = 0x%016llx, dPTS = %lld",
+          mStreamType,
+          PTS,
+          (int64_t)PTS - mPrevPTS);
+    mPrevPTS = PTS;
+#endif
+
+    ALOGV("onPayloadData mStreamType=0x%02x", mStreamType);
+
+    int64_t timeUs = 0ll;  // no presentation timestamp available.
+    if (PTS_DTS_flags == 2 || PTS_DTS_flags == 3) {
+        timeUs = mProgram->convertPTSToTimestamp(PTS);
+    }
+
+    status_t err = mQueue->appendData(data, size, timeUs);
+
+    if (err != OK) {
+        return;
+    }
+
+    sp<ABuffer> accessUnit;
+    while ((accessUnit = mQueue->dequeueAccessUnit()) != NULL) {
+        if (mSource == NULL) {
+            sp<MetaData> meta = mQueue->getFormat();
+
+            if (meta != NULL) {
+                ALOGV("Stream PID 0x%08x of type 0x%02x now has data.",
+                     mElementaryPID, mStreamType);
+
+                mSource = new AnotherPacketSource(meta);
+                mSource->queueAccessUnit(accessUnit);
+            }
+        } else if (mQueue->getFormat() != NULL) {
+            // After a discontinuity we invalidate the queue's format
+            // and won't enqueue any access units to the source until
+            // the queue has reestablished the new format.
+
+            if (mSource->getFormat() == NULL) {
+                mSource->setFormat(mQueue->getFormat());
+            }
+            mSource->queueAccessUnit(accessUnit);
+        }
+    }
+}
+
+sp<AnotherPacketSource> ATSParser::Stream::getSource(SourceType type) {
+    switch (type) {
+        case VIDEO:
+        {
+            if (isVideo()) {
+                return mSource;
+            }
+            break;
+        }
+
+        case AUDIO:
+        {
+            if (isAudio()) {
+                return mSource;
+            }
+            break;
+        }
+
+        default:
+            break;
+    }
+
+    return NULL;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ATSParser::ATSParser(uint32_t flags)
+    : mFlags(flags),
+      mAbsoluteTimeAnchorUs(-1ll),
+      mTimeOffsetValid(false),
+      mTimeOffsetUs(0ll),
+      mNumTSPacketsParsed(0),
+      mNumPCRs(0) {
+    mPSISections[0 /* PID */] = new PSISection;
+}
+
+ATSParser::~ATSParser() {
+}
+
+status_t ATSParser::feedTSPacket(const void *data, size_t size) {
+    CHECK_EQ(size, kTSPacketSize);
+
+    ABitReader br((const uint8_t *)data, kTSPacketSize);
+    return parseTS(&br);
+}
+
+void ATSParser::signalDiscontinuity(
+        DiscontinuityType type, const sp<AMessage> &extra) {
+    if (type == DISCONTINUITY_ABSOLUTE_TIME) {
+        int64_t timeUs;
+        CHECK(extra->findInt64("timeUs", &timeUs));
+
+        CHECK(mPrograms.empty());
+        mAbsoluteTimeAnchorUs = timeUs;
+        return;
+    } else if (type == DISCONTINUITY_TIME_OFFSET) {
+        int64_t offset;
+        CHECK(extra->findInt64("offset", &offset));
+
+        mTimeOffsetValid = true;
+        mTimeOffsetUs = offset;
+        return;
+    }
+
+    for (const auto &program : mPrograms) {
+        program->signalDiscontinuity(type, extra);
+    }
+}
+
+void ATSParser::signalEOS(status_t finalResult) {
+    CHECK_NE(finalResult, (status_t)OK);
+
+    for (const auto &program : mPrograms) {
+        program->signalEOS(finalResult);
+    }
+}
+
+void ATSParser::parseProgramAssociationTable(ABitReader *br) {
+    unsigned table_id = br->getBits(8);
+    ALOGV("  table_id = %u", table_id);
+    CHECK_EQ(table_id, 0x00u);
+
+    unsigned section_syntax_indictor = br->getBits(1);
+    ALOGV("  section_syntax_indictor = %u", section_syntax_indictor);
+    CHECK_EQ(section_syntax_indictor, 1u);
+
+    CHECK_EQ(br->getBits(1), 0u);
+    MY_LOGV("  reserved = %u", br->getBits(2));
+
+    unsigned section_length = br->getBits(12);
+    ALOGV("  section_length = %u", section_length);
+    CHECK_EQ(section_length & 0xc00, 0u);
+
+    MY_LOGV("  transport_stream_id = %u", br->getBits(16));
+    MY_LOGV("  reserved = %u", br->getBits(2));
+    MY_LOGV("  version_number = %u", br->getBits(5));
+    MY_LOGV("  current_next_indicator = %u", br->getBits(1));
+    MY_LOGV("  section_number = %u", br->getBits(8));
+    MY_LOGV("  last_section_number = %u", br->getBits(8));
+
+    size_t numProgramBytes = (section_length - 5 /* header */ - 4 /* crc */);
+    CHECK_EQ((numProgramBytes % 4), 0u);
+
+    for (size_t i = 0; i < numProgramBytes / 4; ++i) {
+        unsigned program_number = br->getBits(16);
+        ALOGV("    program_number = %u", program_number);
+
+        MY_LOGV("    reserved = %u", br->getBits(3));
+
+        if (program_number == 0) {
+            MY_LOGV("    network_PID = 0x%04x", br->getBits(13));
+        } else {
+            unsigned programMapPID = br->getBits(13);
+
+            ALOGV("    program_map_PID = 0x%04x", programMapPID);
+
+            bool found = false;
+            for (const auto &program : mPrograms) {
+                if (program->number() == program_number) {
+                    program->updateProgramMapPID(programMapPID);
+                    found = true;
+                    break;
+                }
+            }
+
+            if (!found) {
+                mPrograms.push_back(
+                        new Program(this, program_number, programMapPID));
+            }
+
+            if (mPSISections.find(programMapPID) == mPSISections.end()) {
+                mPSISections[programMapPID] = new PSISection;
+            }
+        }
+    }
+
+    MY_LOGV("  CRC = 0x%08x", br->getBits(32));
+}
+
+status_t ATSParser::parsePID(
+        ABitReader *br, unsigned PID,
+        unsigned continuity_counter,
+        unsigned payload_unit_start_indicator) {
+    auto it = mPSISections.find(PID);
+
+    if (it != mPSISections.end()) {
+        const sp<PSISection> &section = it->second;
+
+        if (payload_unit_start_indicator) {
+            CHECK(section->isEmpty());
+
+            unsigned skip = br->getBits(8);
+            br->skipBits(skip * 8);
+        }
+
+
+        CHECK((br->numBitsLeft() % 8) == 0);
+        status_t err = section->append(br->data(), br->numBitsLeft() / 8);
+
+        if (err != OK) {
+            return err;
+        }
+
+        if (!section->isComplete()) {
+            return OK;
+        }
+
+        ABitReader sectionBits(section->data(), section->size());
+
+        if (PID == 0) {
+            parseProgramAssociationTable(&sectionBits);
+        } else {
+            bool handled = false;
+            for (const auto &program : mPrograms) {
+                status_t err;
+                if (!program->parsePSISection(PID, &sectionBits, &err)) {
+                    continue;
+                }
+
+                if (err != OK) {
+                    return err;
+                }
+
+                handled = true;
+                break;
+            }
+
+            if (!handled) {
+                mPSISections.erase(it);
+            }
+        }
+
+        section->clear();
+
+        return OK;
+    }
+
+    bool handled = false;
+    for (const auto &program : mPrograms) {
+        status_t err;
+        if (program->parsePID(
+                    PID, continuity_counter, payload_unit_start_indicator,
+                    br, &err)) {
+            if (err != OK) {
+                return err;
+            }
+
+            handled = true;
+            break;
+        }
+    }
+
+    if (!handled) {
+        ALOGV("PID 0x%04x not handled.", PID);
+    }
+
+    return OK;
+}
+
+void ATSParser::parseAdaptationField(ABitReader *br, unsigned PID) {
+    unsigned adaptation_field_length = br->getBits(8);
+
+    if (adaptation_field_length > 0) {
+        unsigned discontinuity_indicator = br->getBits(1);
+
+        if (discontinuity_indicator) {
+            ALOGV("PID 0x%04x: discontinuity_indicator = 1 (!!!)", PID);
+        }
+
+        br->skipBits(2);
+        unsigned PCR_flag = br->getBits(1);
+
+        size_t numBitsRead = 4;
+
+        if (PCR_flag) {
+            br->skipBits(4);
+            uint64_t PCR_base = br->getBits(32);
+            PCR_base = (PCR_base << 1) | br->getBits(1);
+
+            br->skipBits(6);
+            unsigned PCR_ext = br->getBits(9);
+
+            // The number of bytes from the start of the current
+            // MPEG2 transport stream packet up and including
+            // the final byte of this PCR_ext field.
+            size_t byteOffsetFromStartOfTSPacket =
+                (188 - br->numBitsLeft() / 8);
+
+            uint64_t PCR = PCR_base * 300 + PCR_ext;
+
+            ALOGV("PID 0x%04x: PCR = 0x%016llx (%.2f)",
+                  PID, PCR, PCR / 27E6);
+
+            // The number of bytes received by this parser up to and
+            // including the final byte of this PCR_ext field.
+            size_t byteOffsetFromStart =
+                mNumTSPacketsParsed * 188 + byteOffsetFromStartOfTSPacket;
+
+            for (size_t i = 0; i < mPrograms.size(); ++i) {
+                updatePCR(PID, PCR, byteOffsetFromStart);
+            }
+
+            numBitsRead += 52;
+        }
+
+        CHECK_GE(adaptation_field_length * 8, numBitsRead);
+
+        br->skipBits(adaptation_field_length * 8 - numBitsRead);
+    }
+}
+
+status_t ATSParser::parseTS(ABitReader *br) {
+    ALOGV("---");
+
+    unsigned sync_byte = br->getBits(8);
+    CHECK_EQ(sync_byte, 0x47u);
+
+    MY_LOGV("transport_error_indicator = %u", br->getBits(1));
+
+    unsigned payload_unit_start_indicator = br->getBits(1);
+    ALOGV("payload_unit_start_indicator = %u", payload_unit_start_indicator);
+
+    MY_LOGV("transport_priority = %u", br->getBits(1));
+
+    unsigned PID = br->getBits(13);
+    ALOGV("PID = 0x%04x", PID);
+
+    MY_LOGV("transport_scrambling_control = %u", br->getBits(2));
+
+    unsigned adaptation_field_control = br->getBits(2);
+    ALOGV("adaptation_field_control = %u", adaptation_field_control);
+
+    unsigned continuity_counter = br->getBits(4);
+    ALOGV("PID = 0x%04x, continuity_counter = %u", PID, continuity_counter);
+
+    // ALOGI("PID = 0x%04x, continuity_counter = %u", PID, continuity_counter);
+
+    if (adaptation_field_control == 2 || adaptation_field_control == 3) {
+        parseAdaptationField(br, PID);
+    }
+
+    status_t err = OK;
+
+    if (adaptation_field_control == 1 || adaptation_field_control == 3) {
+        err = parsePID(
+                br, PID, continuity_counter, payload_unit_start_indicator);
+    }
+
+    ++mNumTSPacketsParsed;
+
+    return err;
+}
+
+sp<AnotherPacketSource> ATSParser::getSource(SourceType type) {
+    int which = -1;  // any
+
+    for (const auto &program : mPrograms) {
+        if (which >= 0 && (int)program->number() != which) {
+            continue;
+        }
+
+        sp<AnotherPacketSource> source = program->getSource(type);
+
+        if (source != NULL) {
+            return source;
+        }
+    }
+
+    return NULL;
+}
+
+bool ATSParser::PTSTimeDeltaEstablished() {
+    if (mPrograms.empty()) {
+        return false;
+    }
+
+    return mPrograms.front()->PTSTimeDeltaEstablished();
+}
+
+void ATSParser::updatePCR(
+        unsigned /* PID */, uint64_t PCR, size_t byteOffsetFromStart) {
+    ALOGV("PCR 0x%016llx @ %d", PCR, byteOffsetFromStart);
+
+    if (mNumPCRs == 2) {
+        mPCR[0] = mPCR[1];
+        mPCRBytes[0] = mPCRBytes[1];
+        mSystemTimeUs[0] = mSystemTimeUs[1];
+        mNumPCRs = 1;
+    }
+
+    mPCR[mNumPCRs] = PCR;
+    mPCRBytes[mNumPCRs] = byteOffsetFromStart;
+    mSystemTimeUs[mNumPCRs] = ALooper::GetNowUs();
+
+    ++mNumPCRs;
+
+#if 0
+    if (mNumPCRs == 2) {
+        double transportRate =
+            (mPCRBytes[1] - mPCRBytes[0]) * 27E6 / (mPCR[1] - mPCR[0]);
+
+        ALOGV("transportRate = %.2f bytes/sec", transportRate);
+    }
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+ATSParser::PSISection::PSISection() {
+}
+
+ATSParser::PSISection::~PSISection() {
+}
+
+status_t ATSParser::PSISection::append(const void *data, size_t size) {
+    if (mBuffer == NULL || mBuffer->size() + size > mBuffer->capacity()) {
+        size_t newCapacity =
+            (mBuffer == NULL) ? size : mBuffer->capacity() + size;
+
+        newCapacity = (newCapacity + 1023) & ~1023;
+
+        sp<ABuffer> newBuffer = new ABuffer(newCapacity);
+
+        if (mBuffer != NULL) {
+            memcpy(newBuffer->data(), mBuffer->data(), mBuffer->size());
+            newBuffer->setRange(0, mBuffer->size());
+        } else {
+            newBuffer->setRange(0, 0);
+        }
+
+        mBuffer = newBuffer;
+    }
+
+    memcpy(mBuffer->data() + mBuffer->size(), data, size);
+    mBuffer->setRange(0, mBuffer->size() + size);
+
+    return OK;
+}
+
+void ATSParser::PSISection::clear() {
+    if (mBuffer != NULL) {
+        mBuffer->setRange(0, 0);
+    }
+}
+
+bool ATSParser::PSISection::isComplete() const {
+    if (mBuffer == NULL || mBuffer->size() < 3) {
+        return false;
+    }
+
+    unsigned sectionLength = U16_AT(mBuffer->data() + 1) & 0xfff;
+    return mBuffer->size() >= sectionLength + 3;
+}
+
+bool ATSParser::PSISection::isEmpty() const {
+    return mBuffer == NULL || mBuffer->size() == 0;
+}
+
+const uint8_t *ATSParser::PSISection::data() const {
+    return mBuffer == NULL ? NULL : mBuffer->data();
+}
+
+size_t ATSParser::PSISection::size() const {
+    return mBuffer == NULL ? 0 : mBuffer->size();
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/Android.bp b/host/frontend/gcastv2/libandroid/Android.bp
new file mode 100644
index 0000000..7c781ef
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/Android.bp
@@ -0,0 +1,67 @@
+// Copyright (C) 2018 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.
+
+cc_library_static {
+    name: "libandroidglue",
+    host_supported: true,
+    srcs: [
+        "AAtomizer.cpp",
+        "ABitReader.cpp",
+        "ABuffer.cpp",
+        "ALooper.cpp",
+        "ALooperRoster.cpp",
+        "AMessage.cpp",
+        "ANetworkSession.cpp",
+        "AnotherPacketSource.cpp",
+        "ATSParser.cpp",
+        "ESQueue.cpp",
+        "FileSource.cpp",
+        "JSONObject.cpp",
+        "KeyStore.cpp",
+        "MyScopedByteArray.cpp",
+        "MyScopedUTF8String.cpp",
+        "NuMediaExtractor.cpp",
+        "Utils.cpp",
+        "avc_utils.cpp",
+        "base64.cpp",
+        "hexdump.cpp",
+        "ParsedMessage.cpp",
+        "RefBase.cpp",
+        "TSPacketizer.cpp",
+    ],
+    target: {
+        host: {
+            cflags: [
+                "-DTARGET_ANDROID",
+            ],
+        },
+        android: {
+            cflags: [
+                "-DTARGET_ANDROID_DEVICE",
+            ],
+            srcs: [
+                "ADebug.cpp",
+                "JavaThread.cpp",
+                "MyAndroidRuntime.cpp",
+                "MyJNIHelpers.cpp",
+            ],
+        },
+    },
+    shared_libs: [
+        "libbase",
+    ],
+    local_include_dirs: ["include"],
+    export_include_dirs: ["include"],
+}
+
diff --git a/host/frontend/gcastv2/libandroid/AnotherPacketSource.cpp b/host/frontend/gcastv2/libandroid/AnotherPacketSource.cpp
new file mode 100644
index 0000000..5f0722d
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/AnotherPacketSource.cpp
@@ -0,0 +1,264 @@
+/*
+ * 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.
+ */
+
+#include <media/stagefright/AnotherPacketSource.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/MediaBuffer.h>
+#include <media/stagefright/MediaDefs.h>
+#include <media/stagefright/MediaErrors.h>
+#include <media/stagefright/MetaData.h>
+
+namespace android {
+
+const int64_t kNearEOSMarkUs = 2000000ll; // 2 secs
+
+AnotherPacketSource::AnotherPacketSource(const sp<MetaData> &meta)
+    : mIsAudio(false),
+      mFormat(meta),
+      mLastQueuedTimeUs(0),
+      mEOSResult(OK) {
+    const char *mime;
+    CHECK(meta->findCString(kKeyMIMEType, &mime));
+
+    if (!strncasecmp("audio/", mime, 6)) {
+        mIsAudio = true;
+    } else {
+        CHECK(!strncasecmp("video/", mime, 6));
+    }
+}
+
+void AnotherPacketSource::setFormat(const sp<MetaData> &meta) {
+    CHECK(mFormat == NULL);
+    mFormat = meta;
+}
+
+AnotherPacketSource::~AnotherPacketSource() {
+}
+
+status_t AnotherPacketSource::start(MetaData * /* params */) {
+    return OK;
+}
+
+status_t AnotherPacketSource::stop() {
+    return OK;
+}
+
+sp<MetaData> AnotherPacketSource::getFormat() {
+    return mFormat;
+}
+
+status_t AnotherPacketSource::dequeueAccessUnit(sp<ABuffer> *buffer) {
+    buffer->clear();
+
+    Mutex::Autolock autoLock(mLock);
+    while (mEOSResult == OK && mBuffers.empty()) {
+        mCondition.wait(mLock);
+    }
+
+    if (!mBuffers.empty()) {
+        *buffer = *mBuffers.begin();
+        mBuffers.erase(mBuffers.begin());
+
+        int32_t discontinuity;
+        if ((*buffer)->meta()->findInt32("discontinuity", &discontinuity)) {
+            if (wasFormatChange(discontinuity)) {
+                mFormat.clear();
+            }
+
+            return INFO_DISCONTINUITY;
+        }
+
+        return OK;
+    }
+
+    return mEOSResult;
+}
+
+status_t AnotherPacketSource::read(
+        MediaBuffer **out, const ReadOptions *) {
+    *out = NULL;
+
+    Mutex::Autolock autoLock(mLock);
+    while (mEOSResult == OK && mBuffers.empty()) {
+        mCondition.wait(mLock);
+    }
+
+    if (!mBuffers.empty()) {
+        const sp<ABuffer> buffer = *mBuffers.begin();
+        mBuffers.erase(mBuffers.begin());
+
+        int32_t discontinuity;
+        if (buffer->meta()->findInt32("discontinuity", &discontinuity)) {
+            if (wasFormatChange(discontinuity)) {
+                mFormat.clear();
+            }
+
+            return INFO_DISCONTINUITY;
+        } else {
+            int64_t timeUs;
+            CHECK(buffer->meta()->findInt64("timeUs", &timeUs));
+
+            MediaBuffer *mediaBuffer = new MediaBuffer(buffer->size());
+            memcpy(mediaBuffer->data(), buffer->data(), buffer->size());
+
+            mediaBuffer->meta_data()->setInt64(kKeyTime, timeUs);
+
+            *out = mediaBuffer;
+            return OK;
+        }
+    }
+
+    return mEOSResult;
+}
+
+bool AnotherPacketSource::wasFormatChange(
+        int32_t discontinuityType) const {
+    if (mIsAudio) {
+        return (discontinuityType & ATSParser::DISCONTINUITY_AUDIO_FORMAT) != 0;
+    }
+
+    return (discontinuityType & ATSParser::DISCONTINUITY_VIDEO_FORMAT) != 0;
+}
+
+void AnotherPacketSource::queueAccessUnit(const sp<ABuffer> &buffer) {
+    int32_t damaged;
+    if (buffer->meta()->findInt32("damaged", &damaged) && damaged) {
+        // LOG(VERBOSE) << "discarding damaged AU";
+        return;
+    }
+
+    CHECK(buffer->meta()->findInt64("timeUs", &mLastQueuedTimeUs));
+    ALOGV("queueAccessUnit timeUs=%lld us (%.2f secs)", mLastQueuedTimeUs, mLastQueuedTimeUs / 1E6);
+
+    Mutex::Autolock autoLock(mLock);
+    mBuffers.push_back(buffer);
+    mCondition.signal();
+}
+
+void AnotherPacketSource::queueDiscontinuity(
+        ATSParser::DiscontinuityType type,
+        const sp<AMessage> &extra) {
+    Mutex::Autolock autoLock(mLock);
+
+    // Leave only discontinuities in the queue.
+    auto it = mBuffers.begin();
+    while (it != mBuffers.end()) {
+        sp<ABuffer> oldBuffer = *it;
+
+        int32_t oldDiscontinuityType;
+        if (!oldBuffer->meta()->findInt32(
+                    "discontinuity", &oldDiscontinuityType)) {
+            it = mBuffers.erase(it);
+            continue;
+        }
+
+        ++it;
+    }
+
+    mEOSResult = OK;
+    mLastQueuedTimeUs = 0;
+
+    sp<ABuffer> buffer = new ABuffer(0);
+    buffer->meta()->setInt32("discontinuity", static_cast<int32_t>(type));
+    buffer->meta()->setMessage("extra", extra);
+
+    mBuffers.push_back(buffer);
+    mCondition.signal();
+}
+
+void AnotherPacketSource::signalEOS(status_t result) {
+    CHECK(result != OK);
+
+    Mutex::Autolock autoLock(mLock);
+    mEOSResult = result;
+    mCondition.signal();
+}
+
+bool AnotherPacketSource::hasBufferAvailable(status_t *finalResult) {
+    Mutex::Autolock autoLock(mLock);
+    if (!mBuffers.empty()) {
+        return true;
+    }
+
+    *finalResult = mEOSResult;
+    return false;
+}
+
+int64_t AnotherPacketSource::getBufferedDurationUs(status_t *finalResult) {
+    Mutex::Autolock autoLock(mLock);
+
+    *finalResult = mEOSResult;
+
+    if (mBuffers.empty()) {
+        return 0;
+    }
+
+    int64_t time1 = -1;
+    int64_t time2 = -1;
+
+    auto it = mBuffers.begin();
+    while (it != mBuffers.end()) {
+        const sp<ABuffer> &buffer = *it;
+
+        int64_t timeUs;
+        if (buffer->meta()->findInt64("timeUs", &timeUs)) {
+            if (time1 < 0) {
+                time1 = timeUs;
+            }
+
+            time2 = timeUs;
+        } else {
+            // This is a discontinuity, reset everything.
+            time1 = time2 = -1;
+        }
+
+        ++it;
+    }
+
+    return time2 - time1;
+}
+
+status_t AnotherPacketSource::nextBufferTime(int64_t *timeUs) {
+    *timeUs = 0;
+
+    Mutex::Autolock autoLock(mLock);
+
+    if (mBuffers.empty()) {
+        return mEOSResult != OK ? mEOSResult : -EWOULDBLOCK;
+    }
+
+    sp<ABuffer> buffer = *mBuffers.begin();
+    CHECK(buffer->meta()->findInt64("timeUs", timeUs));
+
+    return OK;
+}
+
+bool AnotherPacketSource::isFinished(int64_t duration) const {
+    if (duration > 0) {
+        int64_t diff = duration - mLastQueuedTimeUs;
+        if (diff < kNearEOSMarkUs && diff > -kNearEOSMarkUs) {
+            ALOGV("Detecting EOS due to near end");
+            return true;
+        }
+    }
+    return (mEOSResult != OK);
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/ESQueue.cpp b/host/frontend/gcastv2/libandroid/ESQueue.cpp
new file mode 100644
index 0000000..cd50663
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/ESQueue.cpp
@@ -0,0 +1,1021 @@
+/*
+ * 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.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "ESQueue"
+#include <media/stagefright/foundation/ADebug.h>
+
+#include "ESQueue.h"
+
+#include <media/stagefright/foundation/hexdump.h>
+#include <media/stagefright/foundation/ABitReader.h>
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/AMessage.h>
+#include <media/stagefright/avc_utils.h>
+#include <media/stagefright/MediaErrors.h>
+#include <media/stagefright/MediaDefs.h>
+#include <media/stagefright/MetaData.h>
+#include <media/stagefright/Utils.h>
+
+#include <netinet/in.h>
+
+#include <vector>
+
+namespace android {
+
+ElementaryStreamQueue::ElementaryStreamQueue(Mode mode, uint32_t flags)
+    : mMode(mode),
+      mFlags(flags) {
+}
+
+sp<MetaData> ElementaryStreamQueue::getFormat() {
+    return mFormat;
+}
+
+void ElementaryStreamQueue::clear(bool clearFormat) {
+    if (mBuffer != NULL) {
+        mBuffer->setRange(0, 0);
+    }
+
+    mRangeInfos.clear();
+
+    if (clearFormat) {
+        mFormat.clear();
+    }
+}
+
+static bool IsSeeminglyValidADTSHeader(const uint8_t *ptr, size_t size) {
+    if (size < 3) {
+        // Not enough data to verify header.
+        return false;
+    }
+
+    if (ptr[0] != 0xff || (ptr[1] >> 4) != 0x0f) {
+        return false;
+    }
+
+    unsigned layer = (ptr[1] >> 1) & 3;
+
+    if (layer != 0) {
+        return false;
+    }
+
+    unsigned ID = (ptr[1] >> 3) & 1;
+    unsigned profile_ObjectType = ptr[2] >> 6;
+
+    if (ID == 1 && profile_ObjectType == 3) {
+        // MPEG-2 profile 3 is reserved.
+        return false;
+    }
+
+    return true;
+}
+
+static bool IsSeeminglyValidMPEGAudioHeader(const uint8_t *ptr, size_t size) {
+    if (size < 3) {
+        // Not enough data to verify header.
+        return false;
+    }
+
+    if (ptr[0] != 0xff || (ptr[1] >> 5) != 0x07) {
+        return false;
+    }
+
+    unsigned ID = (ptr[1] >> 3) & 3;
+
+    if (ID == 1) {
+        return false;  // reserved
+    }
+
+    unsigned layer = (ptr[1] >> 1) & 3;
+
+    if (layer == 0) {
+        return false;  // reserved
+    }
+
+    unsigned bitrateIndex = (ptr[2] >> 4);
+
+    if (bitrateIndex == 0x0f) {
+        return false;  // reserved
+    }
+
+    unsigned samplingRateIndex = (ptr[2] >> 2) & 3;
+
+    if (samplingRateIndex == 3) {
+        return false;  // reserved
+    }
+
+    return true;
+}
+
+status_t ElementaryStreamQueue::appendData(
+        const void *data, size_t size, int64_t timeUs) {
+    if (mBuffer == NULL || mBuffer->size() == 0) {
+        switch (mMode) {
+            case H264:
+            case MPEG_VIDEO:
+            {
+#if 0
+                if (size < 4 || memcmp("\x00\x00\x00\x01", data, 4)) {
+                    return ERROR_MALFORMED;
+                }
+#else
+                uint8_t *ptr = (uint8_t *)data;
+
+                ssize_t startOffset = -1;
+                for (size_t i = 0; i + 3 < size; ++i) {
+                    if (!memcmp("\x00\x00\x00\x01", &ptr[i], 4)) {
+                        startOffset = i;
+                        break;
+                    }
+                }
+
+                if (startOffset < 0) {
+                    return ERROR_MALFORMED;
+                }
+
+                if (startOffset > 0) {
+                    ALOGI("found something resembling an H.264/MPEG syncword at "
+                         "offset %ld",
+                         startOffset);
+                }
+
+                data = &ptr[startOffset];
+                size -= startOffset;
+#endif
+                break;
+            }
+
+            case MPEG4_VIDEO:
+            {
+#if 0
+                if (size < 3 || memcmp("\x00\x00\x01", data, 3)) {
+                    return ERROR_MALFORMED;
+                }
+#else
+                uint8_t *ptr = (uint8_t *)data;
+
+                ssize_t startOffset = -1;
+                for (size_t i = 0; i + 2 < size; ++i) {
+                    if (!memcmp("\x00\x00\x01", &ptr[i], 3)) {
+                        startOffset = i;
+                        break;
+                    }
+                }
+
+                if (startOffset < 0) {
+                    return ERROR_MALFORMED;
+                }
+
+                if (startOffset > 0) {
+                    ALOGI("found something resembling an H.264/MPEG syncword at "
+                         "offset %ld",
+                         startOffset);
+                }
+
+                data = &ptr[startOffset];
+                size -= startOffset;
+#endif
+                break;
+            }
+
+            case AAC:
+            {
+                uint8_t *ptr = (uint8_t *)data;
+
+#if 0
+                if (size < 2 || ptr[0] != 0xff || (ptr[1] >> 4) != 0x0f) {
+                    return ERROR_MALFORMED;
+                }
+#else
+                ssize_t startOffset = -1;
+                for (size_t i = 0; i < size; ++i) {
+                    if (IsSeeminglyValidADTSHeader(&ptr[i], size - i)) {
+                        startOffset = i;
+                        break;
+                    }
+                }
+
+                if (startOffset < 0) {
+                    return ERROR_MALFORMED;
+                }
+
+                if (startOffset > 0) {
+                    ALOGI("found something resembling an AAC syncword at offset %ld",
+                         startOffset);
+                }
+
+                data = &ptr[startOffset];
+                size -= startOffset;
+#endif
+                break;
+            }
+
+            case MPEG_AUDIO:
+            {
+                uint8_t *ptr = (uint8_t *)data;
+
+                ssize_t startOffset = -1;
+                for (size_t i = 0; i < size; ++i) {
+                    if (IsSeeminglyValidMPEGAudioHeader(&ptr[i], size - i)) {
+                        startOffset = i;
+                        break;
+                    }
+                }
+
+                if (startOffset < 0) {
+                    return ERROR_MALFORMED;
+                }
+
+                if (startOffset > 0) {
+                    ALOGI("found something resembling an MPEG audio "
+                         "syncword at offset %ld",
+                         startOffset);
+                }
+
+                data = &ptr[startOffset];
+                size -= startOffset;
+                break;
+            }
+
+            case PCM_AUDIO:
+            {
+                break;
+            }
+
+            default:
+                TRESPASS();
+                break;
+        }
+    }
+
+    size_t neededSize = (mBuffer == NULL ? 0 : mBuffer->size()) + size;
+    if (mBuffer == NULL || neededSize > mBuffer->capacity()) {
+        neededSize = (neededSize + 65535) & ~65535;
+
+        ALOGV("resizing buffer to size %d", neededSize);
+
+        sp<ABuffer> buffer = new ABuffer(neededSize);
+        if (mBuffer != NULL) {
+            memcpy(buffer->data(), mBuffer->data(), mBuffer->size());
+            buffer->setRange(0, mBuffer->size());
+        } else {
+            buffer->setRange(0, 0);
+        }
+
+        mBuffer = buffer;
+    }
+
+    memcpy(mBuffer->data() + mBuffer->size(), data, size);
+    mBuffer->setRange(0, mBuffer->size() + size);
+
+    RangeInfo info;
+    info.mLength = size;
+    info.mTimestampUs = timeUs;
+    mRangeInfos.push_back(info);
+
+#if 0
+    if (mMode == AAC) {
+        ALOGI("size = %d, timeUs = %.2f secs", size, timeUs / 1E6);
+        hexdump(data, size);
+    }
+#endif
+
+    return OK;
+}
+
+sp<ABuffer> ElementaryStreamQueue::dequeueAccessUnit() {
+    if ((mFlags & kFlag_AlignedData) && mMode == H264) {
+        if (mRangeInfos.empty()) {
+            return NULL;
+        }
+
+        RangeInfo info = *mRangeInfos.begin();
+        mRangeInfos.erase(mRangeInfos.begin());
+
+        sp<ABuffer> accessUnit = new ABuffer(info.mLength);
+        memcpy(accessUnit->data(), mBuffer->data(), info.mLength);
+        accessUnit->meta()->setInt64("timeUs", info.mTimestampUs);
+
+        memmove(mBuffer->data(),
+                mBuffer->data() + info.mLength,
+                mBuffer->size() - info.mLength);
+
+        mBuffer->setRange(0, mBuffer->size() - info.mLength);
+
+        if (mFormat == NULL) {
+            mFormat = MakeAVCCodecSpecificData(accessUnit);
+        }
+
+        return accessUnit;
+    }
+
+    switch (mMode) {
+        case H264:
+            return dequeueAccessUnitH264();
+        case AAC:
+            return dequeueAccessUnitAAC();
+        case MPEG_VIDEO:
+            return dequeueAccessUnitMPEGVideo();
+        case MPEG4_VIDEO:
+            return dequeueAccessUnitMPEG4Video();
+        case PCM_AUDIO:
+            return dequeueAccessUnitPCMAudio();
+        default:
+            CHECK_EQ((unsigned)mMode, (unsigned)MPEG_AUDIO);
+            return dequeueAccessUnitMPEGAudio();
+    }
+}
+
+sp<ABuffer> ElementaryStreamQueue::dequeueAccessUnitPCMAudio() {
+    if (mBuffer->size() < 4) {
+        return NULL;
+    }
+
+    ABitReader bits(mBuffer->data(), 4);
+    CHECK_EQ(bits.getBits(8), 0xa0u);
+    unsigned numAUs = bits.getBits(8);
+    bits.skipBits(8);
+    // unsigned quantization_word_length = bits.getBits(2);
+    unsigned audio_sampling_frequency = bits.getBits(3);
+    unsigned num_channels = bits.getBits(3);
+
+    CHECK_EQ(audio_sampling_frequency, 2u);  // 48kHz
+    CHECK_EQ(num_channels, 1u);  // stereo!
+
+    if (mFormat == NULL) {
+        mFormat = new MetaData;
+        mFormat->setCString(kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_RAW);
+        mFormat->setInt32(kKeyChannelCount, 2);
+        mFormat->setInt32(kKeySampleRate, 48000);
+    }
+
+    static const size_t kFramesPerAU = 80;
+    size_t frameSize = 2 /* numChannels */ * sizeof(int16_t);
+
+    size_t payloadSize = numAUs * frameSize * kFramesPerAU;
+
+    if (mBuffer->size() < 4 + payloadSize) {
+        return NULL;
+    }
+
+    sp<ABuffer> accessUnit = new ABuffer(payloadSize);
+    memcpy(accessUnit->data(), mBuffer->data() + 4, payloadSize);
+
+    int64_t timeUs = fetchTimestamp(payloadSize + 4);
+    CHECK_GE(timeUs, 0ll);
+    accessUnit->meta()->setInt64("timeUs", timeUs);
+
+    int16_t *ptr = (int16_t *)accessUnit->data();
+    for (size_t i = 0; i < payloadSize / sizeof(int16_t); ++i) {
+        ptr[i] = ntohs(ptr[i]);
+    }
+
+    memmove(
+            mBuffer->data(),
+            mBuffer->data() + 4 + payloadSize,
+            mBuffer->size() - 4 - payloadSize);
+
+    mBuffer->setRange(0, mBuffer->size() - 4 - payloadSize);
+
+    return accessUnit;
+}
+
+sp<ABuffer> ElementaryStreamQueue::dequeueAccessUnitAAC() {
+    int64_t timeUs = 0ll;
+
+    size_t offset = 0;
+    while (offset + 7 <= mBuffer->size()) {
+        ABitReader bits(mBuffer->data() + offset, mBuffer->size() - offset);
+
+        // adts_fixed_header
+
+        CHECK_EQ(bits.getBits(12), 0xfffu);
+        bits.skipBits(4);  // ID, layer, protection_absent
+
+        if (mFormat == NULL) {
+            unsigned profile = bits.getBits(2);
+            CHECK_NE(profile, 3u);
+            unsigned sampling_freq_index = bits.getBits(4);
+            bits.getBits(1);  // private_bit
+            unsigned channel_configuration = bits.getBits(3);
+            CHECK_NE(channel_configuration, 0u);
+            bits.skipBits(2);  // original_copy, home
+
+            mFormat = MakeAACCodecSpecificData(
+                    profile, sampling_freq_index, channel_configuration);
+
+            mFormat->setInt32(kKeyIsADTS, true);
+
+            int32_t sampleRate;
+            int32_t numChannels;
+            CHECK(mFormat->findInt32(kKeySampleRate, &sampleRate));
+            CHECK(mFormat->findInt32(kKeyChannelCount, &numChannels));
+
+            ALOGI("found AAC codec config (%d Hz, %d channels)",
+                 sampleRate, numChannels);
+        } else {
+            // profile_ObjectType, sampling_frequency_index, private_bits,
+            // channel_configuration, original_copy, home
+            bits.skipBits(12);
+        }
+
+        // adts_variable_header
+
+        // copyright_identification_bit, copyright_identification_start
+        bits.skipBits(2);
+
+        unsigned aac_frame_length = bits.getBits(13);
+
+        bits.skipBits(11);  // adts_buffer_fullness
+
+        unsigned number_of_raw_data_blocks_in_frame = bits.getBits(2);
+
+        if (number_of_raw_data_blocks_in_frame != 0) {
+            // To be implemented.
+            TRESPASS();
+        }
+
+        if (offset + aac_frame_length > mBuffer->size()) {
+            break;
+        }
+
+        // size_t headerSize = protection_absent ? 7 : 9;
+
+        int64_t tmpUs = fetchTimestamp(aac_frame_length);
+        CHECK_GE(tmpUs, 0ll);
+
+        if (offset == 0) {
+            timeUs = tmpUs;
+        }
+
+        offset += aac_frame_length;
+    }
+
+    if (offset == 0) {
+        return NULL;
+    }
+
+    sp<ABuffer> accessUnit = new ABuffer(offset);
+    memcpy(accessUnit->data(), mBuffer->data(), offset);
+
+    memmove(mBuffer->data(), mBuffer->data() + offset,
+            mBuffer->size() - offset);
+    mBuffer->setRange(0, mBuffer->size() - offset);
+
+    accessUnit->meta()->setInt64("timeUs", timeUs);
+
+    return accessUnit;
+}
+
+int64_t ElementaryStreamQueue::fetchTimestamp(size_t size) {
+    int64_t timeUs = -1;
+    bool first = true;
+
+    while (size > 0) {
+        CHECK(!mRangeInfos.empty());
+
+        RangeInfo *info = &*mRangeInfos.begin();
+
+        if (first) {
+            timeUs = info->mTimestampUs;
+            first = false;
+        }
+
+        if (info->mLength > size) {
+            info->mLength -= size;
+
+            if (first) {
+                info->mTimestampUs = -1;
+            }
+
+            size = 0;
+        } else {
+            size -= info->mLength;
+
+            mRangeInfos.erase(mRangeInfos.begin());
+            info = NULL;
+        }
+    }
+
+    if (timeUs == 0ll) {
+        ALOGV("Returning 0 timestamp");
+    }
+
+    return timeUs;
+}
+
+struct NALPosition {
+    size_t nalOffset;
+    size_t nalSize;
+};
+
+sp<ABuffer> ElementaryStreamQueue::dequeueAccessUnitH264() {
+    const uint8_t *data = mBuffer->data();
+
+    size_t size = mBuffer->size();
+    std::vector<NALPosition> nals;
+
+    size_t totalSize = 0;
+
+    status_t err;
+    const uint8_t *nalStart;
+    size_t nalSize;
+    bool foundSlice = false;
+    while ((err = getNextNALUnit(&data, &size, &nalStart, &nalSize)) == OK) {
+        if (nalSize == 0) continue;
+
+        unsigned nalType = nalStart[0] & 0x1f;
+        bool flush = false;
+
+        if (nalType == 1 || nalType == 5) {
+            if (foundSlice) {
+                ABitReader br(nalStart + 1, nalSize);
+                unsigned first_mb_in_slice = parseUE(&br);
+
+                if (first_mb_in_slice == 0) {
+                    // This slice starts a new frame.
+
+                    flush = true;
+                }
+            }
+
+            foundSlice = true;
+        } else if ((nalType == 9 || nalType == 7) && foundSlice) {
+            // Access unit delimiter and SPS will be associated with the
+            // next frame.
+
+            flush = true;
+        }
+
+        if (flush) {
+            // The access unit will contain all nal units up to, but excluding
+            // the current one, separated by 0x00 0x00 0x00 0x01 startcodes.
+
+            size_t auSize = 4 * nals.size() + totalSize;
+            sp<ABuffer> accessUnit = new ABuffer(auSize);
+
+#if !LOG_NDEBUG
+            std::string out;
+#endif
+
+            size_t dstOffset = 0;
+            for (size_t i = 0; i < nals.size(); ++i) {
+                const NALPosition &pos = nals.at(i);
+
+                unsigned nalType = mBuffer->data()[pos.nalOffset] & 0x1f;
+
+#if !LOG_NDEBUG
+                char tmp[128];
+                sprintf(tmp, "0x%02x", nalType);
+                if (i > 0) {
+                    out.append(", ");
+                }
+                out.append(tmp);
+#endif
+
+                memcpy(accessUnit->data() + dstOffset, "\x00\x00\x00\x01", 4);
+
+                memcpy(accessUnit->data() + dstOffset + 4,
+                       mBuffer->data() + pos.nalOffset,
+                       pos.nalSize);
+
+                dstOffset += pos.nalSize + 4;
+            }
+
+            ALOGV("accessUnit contains nal types %s", out.c_str());
+
+            const NALPosition &pos = nals.at(nals.size() - 1);
+            size_t nextScan = pos.nalOffset + pos.nalSize;
+
+            memmove(mBuffer->data(),
+                    mBuffer->data() + nextScan,
+                    mBuffer->size() - nextScan);
+
+            mBuffer->setRange(0, mBuffer->size() - nextScan);
+
+            int64_t timeUs = fetchTimestamp(nextScan);
+            CHECK_GE(timeUs, 0ll);
+
+            accessUnit->meta()->setInt64("timeUs", timeUs);
+
+            if (mFormat == NULL) {
+                mFormat = MakeAVCCodecSpecificData(accessUnit);
+            }
+
+            return accessUnit;
+        }
+
+        NALPosition pos;
+        pos.nalOffset = nalStart - mBuffer->data();
+        pos.nalSize = nalSize;
+
+        nals.push_back(pos);
+
+        totalSize += nalSize;
+    }
+    CHECK_EQ(err, (status_t)-EAGAIN);
+
+    return NULL;
+}
+
+sp<ABuffer> ElementaryStreamQueue::dequeueAccessUnitMPEGAudio() {
+    const uint8_t *data = mBuffer->data();
+    size_t size = mBuffer->size();
+
+    if (size < 4) {
+        return NULL;
+    }
+
+    uint32_t header = U32_AT(data);
+
+    size_t frameSize;
+    int samplingRate, numChannels, bitrate, numSamples;
+    CHECK(GetMPEGAudioFrameSize(
+                header, &frameSize, &samplingRate, &numChannels,
+                &bitrate, &numSamples));
+
+    if (size < frameSize) {
+        return NULL;
+    }
+
+    unsigned layer = 4 - ((header >> 17) & 3);
+
+    sp<ABuffer> accessUnit = new ABuffer(frameSize);
+    memcpy(accessUnit->data(), data, frameSize);
+
+    memmove(mBuffer->data(),
+            mBuffer->data() + frameSize,
+            mBuffer->size() - frameSize);
+
+    mBuffer->setRange(0, mBuffer->size() - frameSize);
+
+    int64_t timeUs = fetchTimestamp(frameSize);
+    CHECK_GE(timeUs, 0ll);
+
+    accessUnit->meta()->setInt64("timeUs", timeUs);
+
+    if (mFormat == NULL) {
+        mFormat = new MetaData;
+
+        switch (layer) {
+            case 1:
+                mFormat->setCString(
+                        kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_MPEG_LAYER_I);
+                break;
+            case 2:
+                mFormat->setCString(
+                        kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_MPEG_LAYER_II);
+                break;
+            case 3:
+                mFormat->setCString(
+                        kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_MPEG);
+                break;
+            default:
+                TRESPASS();
+        }
+
+        mFormat->setInt32(kKeySampleRate, samplingRate);
+        mFormat->setInt32(kKeyChannelCount, numChannels);
+    }
+
+    return accessUnit;
+}
+
+static void EncodeSize14(uint8_t **_ptr, size_t size) {
+    CHECK_LE(size, 0x3fffu);
+
+    uint8_t *ptr = *_ptr;
+
+    *ptr++ = 0x80 | (size >> 7);
+    *ptr++ = size & 0x7f;
+
+    *_ptr = ptr;
+}
+
+static sp<ABuffer> MakeMPEGVideoESDS(const sp<ABuffer> &csd) {
+    sp<ABuffer> esds = new ABuffer(csd->size() + 25);
+
+    uint8_t *ptr = esds->data();
+    *ptr++ = 0x03;
+    EncodeSize14(&ptr, 22 + csd->size());
+
+    *ptr++ = 0x00;  // ES_ID
+    *ptr++ = 0x00;
+
+    *ptr++ = 0x00;  // streamDependenceFlag, URL_Flag, OCRstreamFlag
+
+    *ptr++ = 0x04;
+    EncodeSize14(&ptr, 16 + csd->size());
+
+    *ptr++ = 0x40;  // Audio ISO/IEC 14496-3
+
+    for (size_t i = 0; i < 12; ++i) {
+        *ptr++ = 0x00;
+    }
+
+    *ptr++ = 0x05;
+    EncodeSize14(&ptr, csd->size());
+
+    memcpy(ptr, csd->data(), csd->size());
+
+    return esds;
+}
+
+sp<ABuffer> ElementaryStreamQueue::dequeueAccessUnitMPEGVideo() {
+    const uint8_t *data = mBuffer->data();
+    size_t size = mBuffer->size();
+
+    bool sawPictureStart = false;
+    int pprevStartCode = -1;
+    int prevStartCode = -1;
+    int currentStartCode = -1;
+
+    size_t offset = 0;
+    while (offset + 3 < size) {
+        if (memcmp(&data[offset], "\x00\x00\x01", 3)) {
+            ++offset;
+            continue;
+        }
+
+        pprevStartCode = prevStartCode;
+        prevStartCode = currentStartCode;
+        currentStartCode = data[offset + 3];
+
+        if (currentStartCode == 0xb3 && mFormat == NULL) {
+            memmove(mBuffer->data(), mBuffer->data() + offset, size - offset);
+            size -= offset;
+            (void)fetchTimestamp(offset);
+            offset = 0;
+            mBuffer->setRange(0, size);
+        }
+
+        if ((prevStartCode == 0xb3 && currentStartCode != 0xb5)
+                || (pprevStartCode == 0xb3 && prevStartCode == 0xb5)) {
+            // seqHeader without/with extension
+
+            if (mFormat == NULL) {
+                CHECK_GE(size, 7u);
+
+                unsigned width =
+                    (data[4] << 4) | data[5] >> 4;
+
+                unsigned height =
+                    ((data[5] & 0x0f) << 8) | data[6];
+
+                mFormat = new MetaData;
+                mFormat->setCString(kKeyMIMEType, MEDIA_MIMETYPE_VIDEO_MPEG2);
+                mFormat->setInt32(kKeyWidth, width);
+                mFormat->setInt32(kKeyHeight, height);
+
+                ALOGI("found MPEG2 video codec config (%d x %d)", width, height);
+
+                sp<ABuffer> csd = new ABuffer(offset);
+                memcpy(csd->data(), data, offset);
+
+                memmove(mBuffer->data(),
+                        mBuffer->data() + offset,
+                        mBuffer->size() - offset);
+
+                mBuffer->setRange(0, mBuffer->size() - offset);
+                size -= offset;
+                (void)fetchTimestamp(offset);
+                offset = 0;
+
+                // hexdump(csd->data(), csd->size());
+
+                sp<ABuffer> esds = MakeMPEGVideoESDS(csd);
+                mFormat->setData(
+                        kKeyESDS, kTypeESDS, esds->data(), esds->size());
+
+                return NULL;
+            }
+        }
+
+        if (mFormat != NULL && currentStartCode == 0x00) {
+            // Picture start
+
+            if (!sawPictureStart) {
+                sawPictureStart = true;
+            } else {
+                sp<ABuffer> accessUnit = new ABuffer(offset);
+                memcpy(accessUnit->data(), data, offset);
+
+                memmove(mBuffer->data(),
+                        mBuffer->data() + offset,
+                        mBuffer->size() - offset);
+
+                mBuffer->setRange(0, mBuffer->size() - offset);
+
+                int64_t timeUs = fetchTimestamp(offset);
+                CHECK_GE(timeUs, 0ll);
+
+                offset = 0;
+
+                accessUnit->meta()->setInt64("timeUs", timeUs);
+
+                ALOGV("returning MPEG video access unit at time %lld us",
+                      timeUs);
+
+                // hexdump(accessUnit->data(), accessUnit->size());
+
+                return accessUnit;
+            }
+        }
+
+        ++offset;
+    }
+
+    return NULL;
+}
+
+static ssize_t getNextChunkSize(
+        const uint8_t *data, size_t size) {
+    static const char kStartCode[] = "\x00\x00\x01";
+
+    if (size < 3) {
+        return -EAGAIN;
+    }
+
+    if (memcmp(kStartCode, data, 3)) {
+        TRESPASS();
+    }
+
+    size_t offset = 3;
+    while (offset + 2 < size) {
+        if (!memcmp(&data[offset], kStartCode, 3)) {
+            return offset;
+        }
+
+        ++offset;
+    }
+
+    return -EAGAIN;
+}
+
+sp<ABuffer> ElementaryStreamQueue::dequeueAccessUnitMPEG4Video() {
+    uint8_t *data = mBuffer->data();
+    size_t size = mBuffer->size();
+
+    enum {
+        SKIP_TO_VISUAL_OBJECT_SEQ_START,
+        EXPECT_VISUAL_OBJECT_START,
+        EXPECT_VO_START,
+        EXPECT_VOL_START,
+        WAIT_FOR_VOP_START,
+        SKIP_TO_VOP_START,
+
+    } state;
+
+    if (mFormat == NULL) {
+        state = SKIP_TO_VISUAL_OBJECT_SEQ_START;
+    } else {
+        state = SKIP_TO_VOP_START;
+    }
+
+    int32_t width = -1, height = -1;
+
+    size_t offset = 0;
+    ssize_t chunkSize;
+    while ((chunkSize = getNextChunkSize(
+                    &data[offset], size - offset)) > 0) {
+        bool discard = false;
+
+        unsigned chunkType = data[offset + 3];
+
+        switch (state) {
+            case SKIP_TO_VISUAL_OBJECT_SEQ_START:
+            {
+                if (chunkType == 0xb0) {
+                    // Discard anything before this marker.
+
+                    state = EXPECT_VISUAL_OBJECT_START;
+                } else {
+                    discard = true;
+                }
+                break;
+            }
+
+            case EXPECT_VISUAL_OBJECT_START:
+            {
+                CHECK_EQ(chunkType, 0xb5u);
+                state = EXPECT_VO_START;
+                break;
+            }
+
+            case EXPECT_VO_START:
+            {
+                CHECK_LE(chunkType, 0x1fu);
+                state = EXPECT_VOL_START;
+                break;
+            }
+
+            case EXPECT_VOL_START:
+            {
+                CHECK((chunkType & 0xf0) == 0x20);
+
+                CHECK(ExtractDimensionsFromVOLHeader(
+                            &data[offset], chunkSize,
+                            &width, &height));
+
+                state = WAIT_FOR_VOP_START;
+                break;
+            }
+
+            case WAIT_FOR_VOP_START:
+            {
+                if (chunkType == 0xb3 || chunkType == 0xb6) {
+                    // group of VOP or VOP start.
+
+                    mFormat = new MetaData;
+                    mFormat->setCString(
+                            kKeyMIMEType, MEDIA_MIMETYPE_VIDEO_MPEG4);
+
+                    mFormat->setInt32(kKeyWidth, width);
+                    mFormat->setInt32(kKeyHeight, height);
+
+                    ALOGI("found MPEG4 video codec config (%d x %d)",
+                         width, height);
+
+                    sp<ABuffer> csd = new ABuffer(offset);
+                    memcpy(csd->data(), data, offset);
+
+                    // hexdump(csd->data(), csd->size());
+
+                    sp<ABuffer> esds = MakeMPEGVideoESDS(csd);
+                    mFormat->setData(
+                            kKeyESDS, kTypeESDS,
+                            esds->data(), esds->size());
+
+                    discard = true;
+                    state = SKIP_TO_VOP_START;
+                }
+
+                break;
+            }
+
+            case SKIP_TO_VOP_START:
+            {
+                if (chunkType == 0xb6) {
+                    offset += chunkSize;
+
+                    sp<ABuffer> accessUnit = new ABuffer(offset);
+                    memcpy(accessUnit->data(), data, offset);
+
+                    memmove(data, &data[offset], size - offset);
+                    size -= offset;
+                    mBuffer->setRange(0, size);
+
+                    int64_t timeUs = fetchTimestamp(offset);
+                    CHECK_GE(timeUs, 0ll);
+
+                    offset = 0;
+
+                    accessUnit->meta()->setInt64("timeUs", timeUs);
+
+                    ALOGV("returning MPEG4 video access unit at time %lld us",
+                         timeUs);
+
+                    // hexdump(accessUnit->data(), accessUnit->size());
+
+                    return accessUnit;
+                } else if (chunkType != 0xb3) {
+                    offset += chunkSize;
+                    discard = true;
+                }
+
+                break;
+            }
+
+            default:
+                TRESPASS();
+        }
+
+        if (discard) {
+            (void)fetchTimestamp(offset);
+            memmove(data, &data[offset], size - offset);
+            size -= offset;
+            offset = 0;
+            mBuffer->setRange(0, size);
+        } else {
+            offset += chunkSize;
+        }
+    }
+
+    return NULL;
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/ESQueue.h b/host/frontend/gcastv2/libandroid/ESQueue.h
new file mode 100644
index 0000000..9118c24
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/ESQueue.h
@@ -0,0 +1,85 @@
+/*
+ * 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 ES_QUEUE_H_
+
+#define ES_QUEUE_H_
+
+#include <media/stagefright/foundation/ABase.h>
+#include <utils/Errors.h>
+#include <utils/RefBase.h>
+
+#include <list>
+
+namespace android {
+
+struct ABuffer;
+struct MetaData;
+
+struct ElementaryStreamQueue {
+    enum Mode {
+        H264,
+        AAC,
+        MPEG_AUDIO,
+        MPEG_VIDEO,
+        MPEG4_VIDEO,
+        PCM_AUDIO,
+    };
+
+    enum Flags {
+        // Data appended to the queue is always at access unit boundaries.
+        kFlag_AlignedData = 1,
+    };
+    ElementaryStreamQueue(Mode mode, uint32_t flags = 0);
+
+    status_t appendData(const void *data, size_t size, int64_t timeUs);
+    void clear(bool clearFormat);
+
+    sp<ABuffer> dequeueAccessUnit();
+
+    sp<MetaData> getFormat();
+
+private:
+    struct RangeInfo {
+        int64_t mTimestampUs;
+        size_t mLength;
+    };
+
+    Mode mMode;
+    uint32_t mFlags;
+
+    sp<ABuffer> mBuffer;
+    std::list<RangeInfo> mRangeInfos;
+
+    sp<MetaData> mFormat;
+
+    sp<ABuffer> dequeueAccessUnitH264();
+    sp<ABuffer> dequeueAccessUnitAAC();
+    sp<ABuffer> dequeueAccessUnitMPEGAudio();
+    sp<ABuffer> dequeueAccessUnitMPEGVideo();
+    sp<ABuffer> dequeueAccessUnitMPEG4Video();
+    sp<ABuffer> dequeueAccessUnitPCMAudio();
+
+    // consume a logical (compressed) access unit of size "size",
+    // returns its timestamp in us (or -1 if no time information).
+    int64_t fetchTimestamp(size_t size);
+
+    DISALLOW_EVIL_CONSTRUCTORS(ElementaryStreamQueue);
+};
+
+}  // namespace android
+
+#endif  // ES_QUEUE_H_
diff --git a/host/frontend/gcastv2/libandroid/FileSource.cpp b/host/frontend/gcastv2/libandroid/FileSource.cpp
new file mode 100644
index 0000000..986b4f5
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/FileSource.cpp
@@ -0,0 +1,76 @@
+#include <media/stagefright/FileSource.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#ifdef TARGET_IOS
+#include <CoreFoundation/CoreFoundation.h>
+#endif
+
+namespace android {
+
+FileSource::FileSource(const char *path)
+    : mFd(-1),
+      mInitCheck(NO_INIT) {
+#ifdef TARGET_IOS
+    CFBundleRef mainBundle = CFBundleGetMainBundle();
+
+    CFStringRef pathRef =
+        CFStringCreateWithCString(kCFAllocatorDefault, path, kCFStringEncodingUTF8);
+
+    CFURLRef url = CFBundleCopyResourceURL(mainBundle, pathRef, NULL, NULL);
+
+    CFRelease(pathRef);
+    pathRef = nullptr;
+
+    pathRef = CFURLCopyPath(url);
+
+    CFRelease(url);
+    url = nullptr;
+
+    char fullPath[256];
+    CFStringGetCString(
+            pathRef, fullPath, sizeof(fullPath), kCFStringEncodingUTF8);
+
+    CFRelease(pathRef);
+    pathRef = nullptr;
+
+    path = fullPath;
+#endif
+
+    mFd = open(path, O_RDONLY);
+    mInitCheck = (mFd >= 0) ? OK : -errno;
+}
+
+FileSource::~FileSource() {
+    if (mFd >= 0) {
+        close(mFd);
+        mFd = -1;
+    }
+}
+
+status_t FileSource::initCheck() const {
+    return mInitCheck;
+}
+
+status_t FileSource::getSize(off_t *size) const {
+    *size = lseek(mFd, 0, SEEK_END);
+    if (*size == -1) {
+        return -errno;
+    }
+
+    return OK;
+}
+
+ssize_t FileSource::readAt(off_t offset, void *data, size_t size) {
+    ssize_t n = pread(mFd, data, size, offset);
+
+    if (n == -1) {
+        return -errno;
+    }
+
+    return n;
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/JSONObject.cpp b/host/frontend/gcastv2/libandroid/JSONObject.cpp
new file mode 100644
index 0000000..db658ed
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/JSONObject.cpp
@@ -0,0 +1,691 @@
+/*
+ * Copyright (C) 2013 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 "JSONObject"
+#include <utils/Log.h>
+
+#include <media/stagefright/foundation/JSONObject.h>
+
+#include <ctype.h>
+#include <media/stagefright/Utils.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/MediaErrors.h>
+
+namespace android {
+
+// static
+ssize_t JSONValue::Parse(const char *data, size_t size, JSONValue *out) {
+    size_t offset = 0;
+    while (offset < size && isspace(data[offset])) {
+        ++offset;
+    }
+
+    if (offset == size) {
+        return ERROR_MALFORMED;
+    }
+
+    if (data[offset] == '[') {
+        sp<JSONArray> array = new JSONArray;
+        ++offset;
+
+        for (;;) {
+            while (offset < size && isspace(data[offset])) {
+                ++offset;
+            }
+
+            if (offset == size) {
+                return ERROR_MALFORMED;
+            }
+
+            if (data[offset] == ']') {
+                ++offset;
+                break;
+            }
+
+            JSONValue val;
+            ssize_t n = Parse(&data[offset], size - offset, &val);
+
+            if (n < 0) {
+                return n;
+            }
+
+            array->addValue(val);
+
+            offset += n;
+
+            while (offset < size && isspace(data[offset])) {
+                ++offset;
+            }
+
+            if (offset == size) {
+                return ERROR_MALFORMED;
+            }
+
+            if (data[offset] == ',') {
+                ++offset;
+            } else if (data[offset] != ']') {
+                return ERROR_MALFORMED;
+            }
+        };
+
+        out->setArray(array);
+
+        return offset;
+    } else if (data[offset] == '{') {
+        sp<JSONObject> obj = new JSONObject;
+        ++offset;
+
+        for (;;) {
+            while (offset < size && isspace(data[offset])) {
+                ++offset;
+            }
+
+            if (offset == size) {
+                return ERROR_MALFORMED;
+            }
+
+            if (data[offset] == '}') {
+                ++offset;
+                break;
+            }
+
+            JSONValue key;
+            ssize_t n = Parse(&data[offset], size - offset, &key);
+
+            if (n < 0) {
+                return n;
+            }
+
+            if (key.type() != TYPE_STRING) {
+                return ERROR_MALFORMED;
+            }
+
+            offset += n;
+
+            while (offset < size && isspace(data[offset])) {
+                ++offset;
+            }
+
+            if (offset == size || data[offset] != ':') {
+                return ERROR_MALFORMED;
+            }
+
+            ++offset;
+
+            JSONValue val;
+            n = Parse(&data[offset], size - offset, &val);
+
+            if (n < 0) {
+                return n;
+            }
+
+            std::string keyVal;
+            CHECK(key.getString(&keyVal));
+
+            obj->setValue(keyVal.c_str(), val);
+
+            offset += n;
+
+            while (offset < size && isspace(data[offset])) {
+                ++offset;
+            }
+
+            if (offset == size) {
+                return ERROR_MALFORMED;
+            }
+
+            if (data[offset] == ',') {
+                ++offset;
+            } else if (data[offset] != '}') {
+                return ERROR_MALFORMED;
+            }
+        };
+
+        out->setObject(obj);
+
+        return offset;
+    } else if (data[offset] == '"') {
+        ++offset;
+
+        std::string s;
+        bool escaped = false;
+        while (offset < size) {
+            if (escaped) {
+                char c;
+                switch (data[offset]) {
+                    case '\"':
+                    case '\\':
+                    case '/':
+                        c = data[offset];
+                        break;
+                    case 'b':
+                        c = '\x08';
+                        break;
+                    case 'f':
+                        c = '\x0c';
+                        break;
+                    case 'n':
+                        c = '\x0a';
+                        break;
+                    case 'r':
+                        c = '\x0d';
+                        break;
+                    case 't':
+                        c = '\x09';
+                        break;
+                    default:
+                        return ERROR_MALFORMED;
+                }
+
+                s.append(1, c);
+                ++offset;
+
+                escaped = false;
+                continue;
+            } else if (data[offset] == '\\') {
+                escaped = true;
+                ++offset;
+                continue;
+            } else if (data[offset] == '"') {
+                break;
+            }
+
+            s.append(1, data[offset++]);
+        }
+
+        if (offset == size) {
+            return ERROR_MALFORMED;
+        }
+
+        ++offset;
+        out->setString(s);
+
+        return offset;
+    } else if (isdigit(data[offset]) || data[offset] == '-') {
+        bool negate = false;
+        if (data[offset] == '-') {
+            negate = true;
+            ++offset;
+
+            if (offset == size) {
+                return ERROR_MALFORMED;
+            }
+        }
+
+        size_t firstDigitOffset = offset;
+        while (offset < size && isdigit(data[offset])) {
+            ++offset;
+        }
+
+        size_t numDigits = offset - firstDigitOffset;
+        if (numDigits > 1 && data[firstDigitOffset] == '0') {
+            // No leading zeros.
+            return ERROR_MALFORMED;
+        }
+
+        size_t firstFracDigitOffset = 0;
+        size_t numFracDigits = 0;
+
+        if (offset < size && data[offset] == '.') {
+            ++offset;
+
+            firstFracDigitOffset = offset;
+            while (offset < size && isdigit(data[offset])) {
+                ++offset;
+            }
+
+            numFracDigits = offset - firstFracDigitOffset;
+            if (numFracDigits == 0) {
+                return ERROR_MALFORMED;
+            }
+        }
+
+        bool negateExponent = false;
+        size_t firstExpDigitOffset = 0;
+        size_t numExpDigits = 0;
+
+        if (offset < size && (data[offset] == 'e' || data[offset] == 'E')) {
+            ++offset;
+
+            if (offset == size) {
+                return ERROR_MALFORMED;
+            }
+
+            if (data[offset] == '+' || data[offset] == '-') {
+                if (data[offset] == '-') {
+                    negateExponent = true;
+                }
+
+                ++offset;
+            }
+
+            firstExpDigitOffset = offset;
+            while (offset < size && isdigit(data[offset])) {
+                ++offset;
+            }
+
+            numExpDigits = offset - firstExpDigitOffset;
+            if (numExpDigits == 0) {
+                return ERROR_MALFORMED;
+            }
+        }
+
+        CHECK_EQ(numFracDigits, 0u);
+        CHECK_EQ(numExpDigits, 0u);
+
+        int32_t x = 0;
+        for (size_t i = 0; i < numDigits; ++i) {
+            x *= 10;
+            x += data[firstDigitOffset + i] - '0';
+
+            CHECK_GE(x, 0);
+        }
+
+        if (negate) {
+            x = -x;
+        }
+
+        out->setInt32(x);
+
+        return offset;
+    } else if (offset + 4 <= size && !strncmp("null", &data[offset], 4)) {
+        out->unset();
+        return offset + 4;
+    } else if (offset + 4 <= size && !strncmp("true", &data[offset], 4)) {
+        out->setBoolean(true);
+        return offset + 4;
+    } else if (offset + 5 <= size && !strncmp("false", &data[offset], 5)) {
+        out->setBoolean(false);
+        return offset + 5;
+    }
+
+    return ERROR_MALFORMED;
+}
+
+JSONValue::JSONValue()
+    : mType(TYPE_NULL) {
+}
+
+JSONValue::JSONValue(const JSONValue &other)
+    : mType(TYPE_NULL) {
+    *this = other;
+}
+
+JSONValue &JSONValue::operator=(const JSONValue &other) {
+    if (&other != this) {
+        unset();
+        mType = other.mType;
+        mValue = other.mValue;
+
+        switch (mType) {
+            case TYPE_STRING:
+                mValue.mString = new std::string(*other.mValue.mString);
+                break;
+            case TYPE_OBJECT:
+            case TYPE_ARRAY:
+                mValue.mObjectOrArray->incStrong(this);
+                break;
+
+            default:
+                break;
+        }
+    }
+
+    return *this;
+}
+
+JSONValue::~JSONValue() {
+    unset();
+}
+
+JSONValue::FieldType JSONValue::type() const {
+    return mType;
+}
+
+bool JSONValue::getInt32(int32_t *value) const {
+    if (mType != TYPE_NUMBER) {
+        return false;
+    }
+
+    *value = mValue.mInt32;
+    return true;
+}
+
+bool JSONValue::getString(std::string *value) const {
+    if (mType != TYPE_STRING) {
+        return false;
+    }
+
+    *value = *mValue.mString;
+    return true;
+}
+
+bool JSONValue::getBoolean(bool *value) const {
+    if (mType != TYPE_BOOLEAN) {
+        return false;
+    }
+
+    *value = mValue.mBoolean;
+    return true;
+}
+
+bool JSONValue::getObject(sp<JSONObject> *value) const {
+    if (mType != TYPE_OBJECT) {
+        return false;
+    }
+
+    *value = static_cast<JSONObject *>(mValue.mObjectOrArray);
+    return true;
+}
+
+bool JSONValue::getArray(sp<JSONArray> *value) const {
+    if (mType != TYPE_ARRAY) {
+        return false;
+    }
+
+    *value = static_cast<JSONArray *>(mValue.mObjectOrArray);
+    return true;
+}
+
+void JSONValue::setInt32(int32_t value) {
+    unset();
+
+    mValue.mInt32 = value;
+    mType = TYPE_NUMBER;
+}
+
+void JSONValue::setString(std::string_view value) {
+    unset();
+
+    mValue.mString = new std::string(value);
+    mType = TYPE_STRING;
+}
+
+void JSONValue::setBoolean(bool value) {
+    unset();
+
+    mValue.mBoolean = value;
+    mType = TYPE_BOOLEAN;
+}
+
+void JSONValue::setObject(const sp<JSONObject> &obj) {
+    unset();
+
+    mValue.mObjectOrArray = obj.get();
+    mValue.mObjectOrArray->incStrong(this);
+
+    mType = TYPE_OBJECT;
+}
+
+void JSONValue::setArray(const sp<JSONArray> &array) {
+    unset();
+
+    mValue.mObjectOrArray = array.get();
+    mValue.mObjectOrArray->incStrong(this);
+
+    mType = TYPE_ARRAY;
+}
+
+void JSONValue::unset() {
+    switch (mType) {
+        case TYPE_STRING:
+            delete mValue.mString;
+            break;
+        case TYPE_OBJECT:
+        case TYPE_ARRAY:
+            mValue.mObjectOrArray->decStrong(this);
+            break;
+
+        default:
+            break;
+    }
+
+    mType = TYPE_NULL;
+}
+
+static void EscapeString(const char *in, size_t inSize, std::string *out) {
+    CHECK(in != out->c_str());
+    out->clear();
+
+    for (size_t i = 0; i < inSize; ++i) {
+        char c = in[i];
+        switch (c) {
+            case '\"':
+                out->append("\\\"");
+                break;
+            case '\\':
+                out->append("\\\\");
+                break;
+            case '/':
+                out->append("\\/");
+                break;
+            case '\x08':
+                out->append("\\b");
+                break;
+            case '\x0c':
+                out->append("\\f");
+                break;
+            case '\x0a':
+                out->append("\\n");
+                break;
+            case '\x0d':
+                out->append("\\r");
+                break;
+            case '\x09':
+                out->append("\\t");
+                break;
+            default:
+                out->append(1, c);
+                break;
+        }
+    }
+}
+
+std::string JSONValue::toString(size_t depth, bool indentFirstLine) const {
+    static const char kIndent[] = "                                        ";
+
+    std::string out;
+
+    switch (mType) {
+        case TYPE_STRING:
+        {
+            std::string escaped;
+            EscapeString(
+                    mValue.mString->c_str(), mValue.mString->size(), &escaped);
+
+            out.append("\"");
+            out.append(escaped);
+            out.append("\"");
+            break;
+        }
+
+        case TYPE_NUMBER:
+        {
+            out = StringPrintf("%d", mValue.mInt32);
+            break;
+        }
+
+        case TYPE_BOOLEAN:
+        {
+            out = mValue.mBoolean ? "true" : "false";
+            break;
+        }
+
+        case TYPE_NULL:
+        {
+            out = "null";
+            break;
+        }
+
+        case TYPE_OBJECT:
+        case TYPE_ARRAY:
+        {
+            out = (mType == TYPE_OBJECT) ? "{\n" : "[\n";
+            out.append(mValue.mObjectOrArray->internalToString(depth + 1, true));
+            out.append("\n");
+            out.append(kIndent, 2 * depth);
+            out.append(mType == TYPE_OBJECT ? "}" : "]");
+            break;
+        }
+
+        default:
+            TRESPASS();
+    }
+
+    if (indentFirstLine) {
+        out.insert(0, kIndent, 2 * depth);
+    }
+
+    return out;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// static
+sp<JSONCompound> JSONCompound::Parse(const char *data, size_t size) {
+    JSONValue value;
+    ssize_t result = JSONValue::Parse(data, size, &value);
+
+    if (result < 0) {
+        return NULL;
+    }
+
+    sp<JSONObject> obj;
+    if (value.getObject(&obj)) {
+        return obj;
+    }
+
+    sp<JSONArray> array;
+    if (value.getArray(&array)) {
+        return array;
+    }
+
+    return NULL;
+}
+
+std::string JSONCompound::toString(size_t depth, bool indentFirstLine) const {
+    JSONValue val;
+    if (isObject()) {
+        val.setObject((JSONObject *)this);
+    } else {
+        val.setArray((JSONArray *)this);
+    }
+
+    return val.toString(depth, indentFirstLine);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+JSONObject::JSONObject() {}
+JSONObject::~JSONObject() {}
+
+bool JSONObject::isObject() const {
+    return true;
+}
+
+bool JSONObject::getValue(const char *key, JSONValue *value) const {
+    auto it = mValues.find(key);
+
+    if (it == mValues.end()) {
+        return false;
+    }
+
+    *value = it->second;
+
+    return true;
+}
+
+void JSONObject::setValue(const char *key, const JSONValue &value) {
+    mValues[std::string(key)] = value;
+}
+
+void JSONObject::remove(const char *key) {
+    mValues.erase(key);
+}
+
+std::string JSONObject::internalToString(
+        size_t depth, bool /* indentFirstLine */) const {
+    static const char kIndent[] = "                                        ";
+
+    std::string out;
+    for (auto it = mValues.begin(); it != mValues.end();) {
+        const std::string &key = it->first;
+        std::string escapedKey;
+        EscapeString(key.c_str(), key.size(), &escapedKey);
+
+        out.append(kIndent, 2 * depth);
+        out.append("\"");
+        out.append(escapedKey);
+        out.append("\": ");
+
+        out.append(it->second.toString(depth + 1, false));
+
+        ++it;
+        if (it != mValues.end()) {
+            out.append(",\n");
+        }
+    }
+
+    return out;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+JSONArray::JSONArray() {}
+
+JSONArray::~JSONArray() {}
+
+bool JSONArray::isObject() const {
+    return false;
+}
+
+size_t JSONArray::size() const {
+    return mValues.size();
+}
+
+bool JSONArray::getValue(size_t key, JSONValue *value) const {
+    if (key >= mValues.size()) {
+        return false;
+    }
+
+    *value = mValues[key];
+
+    return true;
+}
+
+void JSONArray::addValue(const JSONValue &value) {
+    mValues.push_back(value);
+}
+
+std::string JSONArray::internalToString(
+        size_t depth, bool /* indentFirstLine */) const {
+    std::string out;
+    for (size_t i = 0; i < mValues.size(); ++i) {
+        out.append(mValues[i].toString(depth));
+
+        if (i + 1 < mValues.size()) {
+            out.append(",\n");
+        }
+    }
+
+    return out;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/JavaThread.cpp b/host/frontend/gcastv2/libandroid/JavaThread.cpp
new file mode 100644
index 0000000..e35846f
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/JavaThread.cpp
@@ -0,0 +1,30 @@
+#include <helpers/JavaThread.h>
+
+#include <helpers/MyAndroidRuntime.h>
+#include <media/stagefright/foundation/ADebug.h>
+
+namespace android {
+
+void javaAttachThread() {
+    JavaVMAttachArgs args;
+    args.version = JNI_VERSION_1_4;
+    args.name = (char *)"JavaThread";
+    args.group = nullptr;
+
+    JavaVM *vm = MyAndroidRuntime::getJavaVM();
+    CHECK(vm);
+
+    JNIEnv *env;
+    jint result = vm->AttachCurrentThread(&env, (void *)&args);
+    CHECK_EQ(result, JNI_OK);
+}
+
+void javaDetachThread() {
+    JavaVM *vm = MyAndroidRuntime::getJavaVM();
+    CHECK(vm);
+
+    CHECK_EQ(vm->DetachCurrentThread(), JNI_OK);
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/KeyStore.cpp b/host/frontend/gcastv2/libandroid/KeyStore.cpp
new file mode 100644
index 0000000..83ec9cd
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/KeyStore.cpp
@@ -0,0 +1,30 @@
+#include <utils/KeyStore.h>
+
+#include <unordered_map>
+#include <mutex>
+
+static std::unordered_map<std::string, std::vector<uint8_t>> gCertStore;
+static std::mutex gCertStoreLock;
+
+void setCertificateOrKey(
+        const std::string &name, const void *_data, size_t size) {
+
+    const uint8_t *data = static_cast<const uint8_t *>(_data);
+
+    std::lock_guard autoLock(gCertStoreLock);
+    gCertStore[name] = std::vector<uint8_t>(data, &data[size]);
+}
+
+bool getCertificateOrKey(
+        const std::string &name, std::vector<uint8_t> *data) {
+    std::lock_guard autoLock(gCertStoreLock);
+
+    auto it = gCertStore.find(name);
+    if (it == gCertStore.end()) {
+        return false;
+    }
+
+    *data = it->second;
+
+    return true;
+}
diff --git a/host/frontend/gcastv2/libandroid/MyAndroidRuntime.cpp b/host/frontend/gcastv2/libandroid/MyAndroidRuntime.cpp
new file mode 100644
index 0000000..ae111b0
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/MyAndroidRuntime.cpp
@@ -0,0 +1,28 @@
+#include <helpers/MyAndroidRuntime.h>
+
+namespace android {
+
+static JavaVM *gVM;
+
+// static
+void MyAndroidRuntime::setJavaVM(JavaVM *vm) {
+    gVM = vm;
+}
+
+// static
+JavaVM *MyAndroidRuntime::getJavaVM() {
+    return gVM;
+}
+
+// static
+JNIEnv *MyAndroidRuntime::getJNIEnv() {
+    JNIEnv *env;
+    if (gVM->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
+        return nullptr;
+    }
+
+    return env;
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/MyJNIHelpers.cpp b/host/frontend/gcastv2/libandroid/MyJNIHelpers.cpp
new file mode 100644
index 0000000..f1ed0fe
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/MyJNIHelpers.cpp
@@ -0,0 +1,30 @@
+#include <helpers/MyJNIHelpers.h>
+
+#include <helpers/MyScopedLocalRef.h>
+#include <media/stagefright/foundation/ADebug.h>
+
+namespace android {
+
+void jniThrowException(
+        JNIEnv *env, const char *className, const char *msg) {
+    MyScopedLocalRef<jclass> clazz(env, env->FindClass(className));
+    CHECK(clazz.get() != nullptr);
+
+    CHECK_EQ(env->ThrowNew(clazz.get(), msg), JNI_OK);
+}
+
+int jniRegisterNativeMethods(
+        JNIEnv *env,
+        const char *className,
+        const JNINativeMethod *methods,
+        size_t numMethods) {
+    MyScopedLocalRef<jclass> clazz(env, env->FindClass(className));
+    CHECK(clazz.get() != nullptr);
+
+    CHECK_GE(env->RegisterNatives(clazz.get(), methods, numMethods), 0);
+
+    return 0;
+}
+
+}  // namespaced android
+
diff --git a/host/frontend/gcastv2/libandroid/MyScopedByteArray.cpp b/host/frontend/gcastv2/libandroid/MyScopedByteArray.cpp
new file mode 100644
index 0000000..6f31d38
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/MyScopedByteArray.cpp
@@ -0,0 +1,31 @@
+#include <helpers/MyScopedByteArray.h>
+
+namespace android {
+
+MyScopedByteArray::MyScopedByteArray(JNIEnv *env, jbyteArray arrayObj)
+    : mEnv(env),
+      mArrayObj(arrayObj),
+      mElements(nullptr),
+      mSize(0) {
+    if (mArrayObj) {
+        mElements = env->GetByteArrayElements(mArrayObj, nullptr /* isCopy */);
+        mSize = env->GetArrayLength(mArrayObj);
+    }
+}
+
+MyScopedByteArray::~MyScopedByteArray() {
+    if (mArrayObj) {
+        mEnv->ReleaseByteArrayElements(mArrayObj, mElements, 0 /* mode */);
+    }
+}
+
+const jbyte *MyScopedByteArray::data() const {
+    return mElements;
+}
+
+jsize MyScopedByteArray::size() const {
+    return mSize;
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/MyScopedUTF8String.cpp b/host/frontend/gcastv2/libandroid/MyScopedUTF8String.cpp
new file mode 100644
index 0000000..2f7e3fe
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/MyScopedUTF8String.cpp
@@ -0,0 +1,22 @@
+#include <helpers/MyScopedUTF8String.h>
+
+namespace android {
+
+MyScopedUTF8String::MyScopedUTF8String(JNIEnv *env, jstring stringObj)
+    : mEnv(env),
+      mStringObj(stringObj),
+      mData(stringObj ? env->GetStringUTFChars(stringObj, nullptr) : nullptr) {
+}
+
+MyScopedUTF8String::~MyScopedUTF8String() {
+    if (mData) {
+        mEnv->ReleaseStringUTFChars(mStringObj, mData);
+        mData = nullptr;
+    }
+}
+
+const char *MyScopedUTF8String::c_str() const {
+    return mData;
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/NuMediaExtractor.cpp b/host/frontend/gcastv2/libandroid/NuMediaExtractor.cpp
new file mode 100644
index 0000000..cce5696
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/NuMediaExtractor.cpp
@@ -0,0 +1,241 @@
+#include <media/stagefright/NuMediaExtractor.h>
+
+#include <media/stagefright/AnotherPacketSource.h>
+#include <media/stagefright/ATSParser.h>
+#include <media/stagefright/MediaErrors.h>
+#include <media/stagefright/MetaData.h>
+#include <media/stagefright/Utils.h>
+
+namespace android {
+
+NuMediaExtractor::NuMediaExtractor()
+    : mFlags(0),
+      mParser(new ATSParser),
+      mFile(NULL),
+      mNumTracks(0),
+      mAudioTrackIndex(-1),
+      mVideoTrackIndex(-1),
+      mNextIndex(-1) {
+    mFinalResult[0] = mFinalResult[1] = OK;
+}
+
+NuMediaExtractor::~NuMediaExtractor() {
+    if (mFile) {
+        fclose(mFile);
+        mFile = NULL;
+    }
+}
+
+status_t NuMediaExtractor::setDataSource(const char *path) {
+    if (mFile) {
+        return UNKNOWN_ERROR;
+    }
+
+    mFile = fopen(path, "rb");
+
+    if (!mFile) {
+        return -ENOENT;
+    }
+
+    for (size_t i = 0; i < 1024; ++i) {
+        if (mVideoSource == NULL) {
+            mVideoSource = mParser->getSource(ATSParser::VIDEO);
+        }
+
+        if (mAudioSource == NULL) {
+            mAudioSource = mParser->getSource(ATSParser::AUDIO);
+        }
+
+        if (feedMoreData() != OK) {
+            break;
+        }
+    }
+
+    if (mAudioSource != NULL && mAudioSource->getFormat() == NULL) {
+        mAudioSource.clear();
+    }
+
+    if (mVideoSource != NULL && mVideoSource->getFormat() == NULL) {
+        mVideoSource.clear();
+    }
+
+    mNumTracks = 0;
+    if (mAudioSource != NULL) {
+        mAudioTrackIndex = mNumTracks;
+        ++mNumTracks;
+    }
+    if (mVideoSource != NULL) {
+        mVideoTrackIndex = mNumTracks;
+        ++mNumTracks;
+    }
+
+    return OK;
+}
+
+size_t NuMediaExtractor::countTracks() const {
+    return mNumTracks;
+}
+
+status_t NuMediaExtractor::getTrackFormat(
+        size_t index, sp<AMessage> *format) const {
+    CHECK_LT(index, mNumTracks);
+
+    sp<MetaData> meta;
+    if ((ssize_t)index == mAudioTrackIndex) {
+        meta = mAudioSource->getFormat();
+    } else {
+        meta = mVideoSource->getFormat();
+    }
+
+    return convertMetaDataToMessage(meta, format);
+}
+
+status_t NuMediaExtractor::selectTrack(size_t index) {
+    CHECK_LT(index, mNumTracks);
+
+    if ((ssize_t)index == mAudioTrackIndex) {
+        mFlags |= FLAG_AUDIO_SELECTED;
+    } else {
+        mFlags |= FLAG_VIDEO_SELECTED;
+    }
+
+    return OK;
+}
+
+status_t NuMediaExtractor::getSampleTime(int64_t *timeUs) {
+    fetchSamples();
+
+    if (mNextIndex < 0) {
+        return ERROR_END_OF_STREAM;
+    }
+
+    CHECK(mNextBuffer[mNextIndex]->meta()->findInt64("timeUs", timeUs));
+
+    return OK;
+}
+
+status_t NuMediaExtractor::getSampleTrackIndex(size_t *index) {
+    fetchSamples();
+
+    if (mNextIndex < 0) {
+        return ERROR_END_OF_STREAM;
+    }
+
+    *index = mNextIndex;
+
+    return OK;
+}
+
+status_t NuMediaExtractor::readSampleData(sp<ABuffer> accessUnit) {
+    fetchSamples();
+
+    if (mNextIndex < 0) {
+        return ERROR_END_OF_STREAM;
+    }
+
+    accessUnit->setRange(0, mNextBuffer[mNextIndex]->size());
+
+    memcpy(accessUnit->data(),
+           mNextBuffer[mNextIndex]->data(),
+           mNextBuffer[mNextIndex]->size());
+
+    return OK;
+}
+
+status_t NuMediaExtractor::advance() {
+    if (mNextIndex < 0) {
+        return ERROR_END_OF_STREAM;
+    }
+
+    mNextBuffer[mNextIndex].clear();
+    mNextIndex = -1;
+
+    return OK;
+}
+
+void NuMediaExtractor::fetchSamples() {
+    status_t err;
+
+    if ((mFlags & FLAG_AUDIO_SELECTED)
+            && mNextBuffer[mAudioTrackIndex] == NULL
+            && mFinalResult[mAudioTrackIndex] == OK) {
+        status_t finalResult;
+        while (!mAudioSource->hasBufferAvailable(&finalResult)
+                && finalResult == OK) {
+            feedMoreData();
+        }
+
+        err = mAudioSource->dequeueAccessUnit(&mNextBuffer[mAudioTrackIndex]);
+
+        if (err != OK) {
+            mFinalResult[mAudioTrackIndex] = err;
+        }
+    }
+
+    if ((mFlags & FLAG_VIDEO_SELECTED)
+            && mNextBuffer[mVideoTrackIndex] == NULL
+            && mFinalResult[mVideoTrackIndex] == OK) {
+        status_t finalResult;
+        while (!mVideoSource->hasBufferAvailable(&finalResult)
+                && finalResult == OK) {
+            feedMoreData();
+        }
+
+        err = mVideoSource->dequeueAccessUnit(&mNextBuffer[mVideoTrackIndex]);
+
+        if (err != OK) {
+            mFinalResult[mVideoTrackIndex] = err;
+        }
+    }
+
+    bool haveValidTime = false;
+    int64_t minTimeUs = -1ll;
+    size_t minIndex = 0;
+
+    if ((mFlags & FLAG_AUDIO_SELECTED)
+            && mNextBuffer[mAudioTrackIndex] != NULL) {
+        CHECK(mNextBuffer[mAudioTrackIndex]->meta()->findInt64("timeUs", &minTimeUs));
+        haveValidTime = true;
+        minIndex = mAudioTrackIndex;
+    }
+
+    if ((mFlags & FLAG_VIDEO_SELECTED)
+            && mNextBuffer[mVideoTrackIndex] != NULL) {
+        int64_t timeUs;
+        CHECK(mNextBuffer[mVideoTrackIndex]->meta()->findInt64("timeUs", &timeUs));
+
+        if (!haveValidTime || timeUs < minTimeUs) {
+            haveValidTime = true;
+            minTimeUs = timeUs;
+            minIndex = mVideoTrackIndex;
+        }
+    }
+
+    if (!haveValidTime) {
+        mNextIndex = -1;
+    } else {
+        mNextIndex = minIndex;
+    }
+}
+
+status_t NuMediaExtractor::feedMoreData() {
+    uint8_t packet[188];
+    ssize_t n = fread(packet, 1, sizeof(packet), mFile);
+
+    status_t err;
+
+    if (n < (ssize_t)sizeof(packet)) {
+        err = UNKNOWN_ERROR;
+    } else {
+        err = mParser->feedTSPacket(packet, sizeof(packet));
+    }
+
+    if (err != OK) {
+        mParser->signalEOS(err);
+        return err;
+    }
+
+    return OK;
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/ParsedMessage.cpp b/host/frontend/gcastv2/libandroid/ParsedMessage.cpp
new file mode 100644
index 0000000..132ded9
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/ParsedMessage.cpp
@@ -0,0 +1,303 @@
+/*
+ * 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.
+ */
+
+#include <media/stagefright/foundation/ParsedMessage.h>
+
+#include <media/stagefright/Utils.h>
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/hexdump.h>
+
+#include <ctype.h>
+
+namespace android {
+
+// static
+sp<ParsedMessage> ParsedMessage::Parse(
+        const char *data, size_t size, bool noMoreData, size_t *length) {
+    sp<ParsedMessage> msg = new ParsedMessage;
+    ssize_t res = msg->parse(data, size, noMoreData);
+
+    if (res < 0) {
+        *length = 0;
+        return NULL;
+    }
+
+    *length = res;
+    return msg;
+}
+
+ParsedMessage::ParsedMessage() {
+}
+
+ParsedMessage::~ParsedMessage() {
+}
+
+bool ParsedMessage::findString(const char *name, std::string *value) const {
+    std::string key = name;
+    toLower(&key);
+
+    auto it = mDict.find(key);
+
+    if (it == mDict.end()) {
+        value->clear();
+
+        return false;
+    }
+
+    *value = it->second;
+    return true;
+}
+
+bool ParsedMessage::findInt32(const char *name, int32_t *value) const {
+    std::string stringValue;
+
+    if (!findString(name, &stringValue)) {
+        return false;
+    }
+
+    char *end;
+    *value = (int32_t)strtol(stringValue.c_str(), &end, 10);
+
+    if (end == stringValue.c_str() || *end != '\0') {
+        *value = 0;
+        return false;
+    }
+
+    return true;
+}
+
+const char *ParsedMessage::getContent() const {
+    return mContent.c_str();
+}
+
+ssize_t ParsedMessage::parse(const char *data, size_t size, bool noMoreData) {
+    if (size == 0) {
+        return -1;
+    }
+
+    auto lastDictIter = mDict.end();
+
+    size_t offset = 0;
+    bool headersComplete = false;
+    while (offset < size) {
+        size_t lineEndOffset = offset;
+        while (lineEndOffset + 1 < size
+                && (data[lineEndOffset] != '\r'
+                        || data[lineEndOffset + 1] != '\n')) {
+            ++lineEndOffset;
+        }
+
+        if (lineEndOffset + 1 >= size) {
+            return -1;
+        }
+
+        std::string line(&data[offset], lineEndOffset - offset);
+
+        if (offset == 0) {
+            // Special handling for the request/status line.
+
+            mDict[std::string("_")] = line;
+            offset = lineEndOffset + 2;
+
+            continue;
+        }
+
+        if (lineEndOffset == offset) {
+            // An empty line separates headers from body.
+            headersComplete = true;
+            offset += 2;
+            break;
+        }
+
+        if (line.c_str()[0] == ' ' || line.c_str()[0] == '\t') {
+            // Support for folded header values.
+
+            if (lastDictIter != mDict.end()) {
+                // Otherwise it's malformed since the first header line
+                // cannot continue anything...
+
+                std::string &value = lastDictIter->second;
+                value.append(line);
+            }
+
+            offset = lineEndOffset + 2;
+            continue;
+        }
+
+        ssize_t colonPos = line.find(":");
+        if (colonPos >= 0) {
+            std::string key(line, 0, colonPos);
+            trim(&key);
+            toLower(&key);
+
+            line.erase(0, colonPos + 1);
+
+            lastDictIter = mDict.insert(std::make_pair(key, line)).first;
+        }
+
+        offset = lineEndOffset + 2;
+    }
+
+    if (!headersComplete && (!noMoreData || offset == 0)) {
+        // We either saw the empty line separating headers from body
+        // or we saw at least the status line and know that no more data
+        // is going to follow.
+        return -1;
+    }
+
+    for (auto &pair : mDict) {
+        trim(&pair.second);
+    }
+
+    int32_t contentLength;
+    if (!findInt32("content-length", &contentLength) || contentLength < 0) {
+        contentLength = 0;
+    }
+
+    size_t totalLength = offset + contentLength;
+
+    if (size < totalLength) {
+        return -1;
+    }
+
+    mContent.assign(&data[offset], contentLength);
+
+    return totalLength;
+}
+
+bool ParsedMessage::getRequestField(size_t index, std::string *field) const {
+    std::string line;
+    CHECK(findString("_", &line));
+
+    size_t prevOffset = 0;
+    size_t offset = 0;
+    for (size_t i = 0; i <= index; ++i) {
+        if (offset >= line.size()) {
+            return false;
+        }
+
+        ssize_t spacePos = line.find(" ", offset);
+
+        if (spacePos < 0) {
+            spacePos = line.size();
+        }
+
+        prevOffset = offset;
+        offset = spacePos + 1;
+    }
+
+    field->assign(line, prevOffset, offset - prevOffset - 1);
+
+    return true;
+}
+
+bool ParsedMessage::getStatusCode(int32_t *statusCode) const {
+    std::string statusCodeString;
+    if (!getRequestField(1, &statusCodeString)) {
+        return false;
+    }
+
+    char *end;
+    *statusCode = (int32_t)strtol(statusCodeString.c_str(), &end, 10);
+
+    if (*end != '\0' || end == statusCodeString.c_str()
+            || (*statusCode) < 100 || (*statusCode) > 999) {
+        *statusCode = 0;
+        return false;
+    }
+
+    return true;
+}
+
+std::string ParsedMessage::debugString() const {
+    std::string line;
+    CHECK(findString("_", &line));
+
+    line.append("\n");
+
+    for (const auto &pair : mDict) {
+        const std::string &key = pair.first;
+        const std::string &value = pair.second;
+
+        if (key == std::string("_")) {
+            continue;
+        }
+
+        line.append(key);
+        line.append(": ");
+        line.append(value);
+        line.append("\n");
+    }
+
+    line.append("\n");
+    line.append(mContent);
+
+    return line;
+}
+
+// static
+bool ParsedMessage::GetAttribute(
+        const char *s, const char *key, std::string *value) {
+    value->clear();
+
+    size_t keyLen = strlen(key);
+
+    for (;;) {
+        while (isspace(*s)) {
+            ++s;
+        }
+
+        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->assign(&s[keyLen + 1], len - keyLen - 1);
+            return true;
+        }
+
+        if (colonPos == NULL) {
+            return false;
+        }
+
+        s = colonPos + 1;
+    }
+}
+
+// static
+bool ParsedMessage::GetInt32Attribute(
+        const char *s, const char *key, int32_t *value) {
+    std::string stringValue;
+    if (!GetAttribute(s, key, &stringValue)) {
+        *value = 0;
+        return false;
+    }
+
+    char *end;
+    *value = (int32_t)strtol(stringValue.c_str(), &end, 10);
+
+    if (end == stringValue.c_str() || *end != '\0') {
+        *value = 0;
+        return false;
+    }
+
+    return true;
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/RefBase.cpp b/host/frontend/gcastv2/libandroid/RefBase.cpp
new file mode 100644
index 0000000..920c561
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/RefBase.cpp
@@ -0,0 +1,15 @@
+#include <utils/RefBase.h>
+
+namespace android {
+
+RefBase *WeakList::promote() const {
+    if (mObject == NULL) {
+        return NULL;
+    }
+
+    mObject->incStrong(this);
+    return mObject;
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/TSPacketizer.cpp b/host/frontend/gcastv2/libandroid/TSPacketizer.cpp
new file mode 100644
index 0000000..f6c4436
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/TSPacketizer.cpp
@@ -0,0 +1,697 @@
+/*
+ * 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 "TSPacketizer"
+#include <utils/Log.h>
+
+#include <media/stagefright/foundation/TSPacketizer.h>
+
+#include <media/stagefright/avc_utils.h>
+#include <media/stagefright/Utils.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/MediaDefs.h>
+#include <media/stagefright/MediaErrors.h>
+
+#include <arpa/inet.h>
+
+#include <vector>
+
+namespace android {
+
+struct TSPacketizer::Track : public RefBase {
+    Track(const sp<AMessage> &format,
+          unsigned PID, unsigned streamType, unsigned streamID);
+
+    unsigned PID() const;
+    unsigned streamType() const;
+    unsigned streamID() const;
+
+    // Returns the previous value.
+    unsigned incrementContinuityCounter();
+
+    bool isAudio() const;
+    bool isVideo() const;
+
+    bool isH264() const;
+    bool lacksADTSHeader() const;
+
+    sp<ABuffer> prependCSD(const sp<ABuffer> &accessUnit) const;
+    sp<ABuffer> prependADTSHeader(const sp<ABuffer> &accessUnit) const;
+
+protected:
+    virtual ~Track();
+
+private:
+    sp<AMessage> mFormat;
+
+    unsigned mPID;
+    unsigned mStreamType;
+    unsigned mStreamID;
+    unsigned mContinuityCounter;
+
+    std::string mMIME;
+    std::vector<sp<ABuffer>> mCSD;
+
+    DISALLOW_EVIL_CONSTRUCTORS(Track);
+};
+
+TSPacketizer::Track::Track(
+        const sp<AMessage> &format,
+        unsigned PID, unsigned streamType, unsigned streamID)
+    : mFormat(format),
+      mPID(PID),
+      mStreamType(streamType),
+      mStreamID(streamID),
+      mContinuityCounter(0) {
+    CHECK(format->findString("mime", &mMIME));
+
+    if (!strcasecmp(mMIME.c_str(), MEDIA_MIMETYPE_VIDEO_AVC)
+            || !strcasecmp(mMIME.c_str(), MEDIA_MIMETYPE_AUDIO_AAC)) {
+        for (size_t i = 0;; ++i) {
+            sp<ABuffer> csd;
+            if (!format->findBuffer(StringPrintf("csd-%d", i).c_str(), &csd)) {
+                break;
+            }
+
+            mCSD.push_back(csd);
+        }
+    }
+}
+
+TSPacketizer::Track::~Track() {
+}
+
+unsigned TSPacketizer::Track::PID() const {
+    return mPID;
+}
+
+unsigned TSPacketizer::Track::streamType() const {
+    return mStreamType;
+}
+
+unsigned TSPacketizer::Track::streamID() const {
+    return mStreamID;
+}
+
+unsigned TSPacketizer::Track::incrementContinuityCounter() {
+    unsigned prevCounter = mContinuityCounter;
+
+    if (++mContinuityCounter == 16) {
+        mContinuityCounter = 0;
+    }
+
+    return prevCounter;
+}
+
+bool TSPacketizer::Track::isAudio() const {
+    return !strncasecmp("audio/", mMIME.c_str(), 6);
+}
+
+bool TSPacketizer::Track::isVideo() const {
+    return !strncasecmp("video/", mMIME.c_str(), 6);
+}
+
+bool TSPacketizer::Track::isH264() const {
+    return !strcasecmp(mMIME.c_str(), MEDIA_MIMETYPE_VIDEO_AVC);
+}
+
+bool TSPacketizer::Track::lacksADTSHeader() const {
+    if (strcasecmp(mMIME.c_str(), MEDIA_MIMETYPE_AUDIO_AAC)) {
+        return false;
+    }
+
+    int32_t isADTS;
+    if (mFormat->findInt32("is-adts", &isADTS) && isADTS != 0) {
+        return false;
+    }
+
+    return true;
+}
+
+sp<ABuffer> TSPacketizer::Track::prependCSD(
+        const sp<ABuffer> &accessUnit) const {
+    size_t size = 0;
+    for (const auto &csd : mCSD) {
+        size += csd->size();
+    }
+
+    sp<ABuffer> dup = new ABuffer(accessUnit->size() + size);
+    size_t offset = 0;
+    for (const auto &csd : mCSD) {
+        memcpy(dup->data() + offset, csd->data(), csd->size());
+        offset += csd->size();
+    }
+
+    memcpy(dup->data() + offset, accessUnit->data(), accessUnit->size());
+
+    return dup;
+}
+
+sp<ABuffer> TSPacketizer::Track::prependADTSHeader(
+        const sp<ABuffer> &accessUnit) const {
+    CHECK_EQ(mCSD.size(), 1u);
+
+    const uint8_t *codec_specific_data = mCSD[0]->data();
+
+    const uint32_t aac_frame_length = static_cast<uint32_t>(accessUnit->size() + 7);
+
+    sp<ABuffer> dup = new ABuffer(aac_frame_length);
+
+    unsigned profile = (codec_specific_data[0] >> 3) - 1;
+
+    unsigned sampling_freq_index =
+        ((codec_specific_data[0] & 7) << 1)
+        | (codec_specific_data[1] >> 7);
+
+    unsigned channel_configuration =
+        (codec_specific_data[1] >> 3) & 0x0f;
+
+    uint8_t *ptr = dup->data();
+
+    *ptr++ = 0xff;
+    *ptr++ = 0xf1;  // b11110001, ID=0, layer=0, protection_absent=1
+
+    *ptr++ =
+        profile << 6
+        | sampling_freq_index << 2
+        | ((channel_configuration >> 2) & 1);  // private_bit=0
+
+    // original_copy=0, home=0, copyright_id_bit=0, copyright_id_start=0
+    *ptr++ =
+        (channel_configuration & 3) << 6
+        | aac_frame_length >> 11;
+    *ptr++ = (aac_frame_length >> 3) & 0xff;
+    *ptr++ = (aac_frame_length & 7) << 5;
+
+    // adts_buffer_fullness=0, number_of_raw_data_blocks_in_frame=0
+    *ptr++ = 0;
+
+    memcpy(ptr, accessUnit->data(), accessUnit->size());
+
+    return dup;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TSPacketizer::TSPacketizer()
+    : mPATContinuityCounter(0),
+      mPMTContinuityCounter(0) {
+    initCrcTable();
+}
+
+TSPacketizer::~TSPacketizer() {
+}
+
+ssize_t TSPacketizer::addTrack(const sp<AMessage> &format) {
+    std::string mime;
+    CHECK(format->findString("mime", &mime));
+
+    unsigned PIDStart;
+    bool isVideo = !strncasecmp("video/", mime.c_str(), 6);
+    bool isAudio = !strncasecmp("audio/", mime.c_str(), 6);
+
+    if (isVideo) {
+        PIDStart = 0x1011;
+    } else if (isAudio) {
+        PIDStart = 0x1100;
+    } else {
+        return ERROR_UNSUPPORTED;
+    }
+
+    unsigned streamType;
+    unsigned streamIDStart;
+    unsigned streamIDStop;
+
+    if (!strcasecmp(mime.c_str(), MEDIA_MIMETYPE_VIDEO_AVC)) {
+        streamType = 0x1b;
+        streamIDStart = 0xe0;
+        streamIDStop = 0xef;
+    } else if (!strcasecmp(mime.c_str(), MEDIA_MIMETYPE_AUDIO_AAC)) {
+        streamType = 0x0f;
+        streamIDStart = 0xc0;
+        streamIDStop = 0xdf;
+    } else {
+        return ERROR_UNSUPPORTED;
+    }
+
+    size_t numTracksOfThisType = 0;
+    unsigned PID = PIDStart;
+
+    for (const auto &track : mTracks) {
+        if (track->streamType() == streamType) {
+            ++numTracksOfThisType;
+        }
+
+        if ((isAudio && track->isAudio()) || (isVideo && track->isVideo())) {
+            ++PID;
+        }
+    }
+
+    unsigned streamID = static_cast<unsigned>(streamIDStart + numTracksOfThisType);
+    if (streamID > streamIDStop) {
+        return -ERANGE;
+    }
+
+    sp<Track> track = new Track(format, PID, streamType, streamID);
+    size_t index = mTracks.size();
+    mTracks.push_back(track);
+    return index;
+}
+
+status_t TSPacketizer::packetize(
+        size_t trackIndex,
+        const sp<ABuffer> &_accessUnit,
+        sp<ABuffer> *packets,
+        uint32_t flags) {
+    sp<ABuffer> accessUnit = _accessUnit;
+
+    packets->clear();
+
+    if (trackIndex >= mTracks.size()) {
+        return -ERANGE;
+    }
+
+    int64_t timeUs;
+    CHECK(accessUnit->meta()->findInt64("timeUs", &timeUs));
+
+    const sp<Track> &track = mTracks[trackIndex];
+
+    if (track->isH264()) {
+        if (IsIDR(accessUnit)) {
+            // prepend codec specific data, i.e. SPS and PPS.
+            accessUnit = track->prependCSD(accessUnit);
+        }
+    } else if (track->lacksADTSHeader()) {
+        accessUnit = track->prependADTSHeader(accessUnit);
+    }
+
+    // 0x47
+    // transport_error_indicator = b0
+    // payload_unit_start_indicator = b1
+    // transport_priority = b0
+    // PID
+    // transport_scrambling_control = b00
+    // adaptation_field_control = b??
+    // continuity_counter = b????
+    // -- payload follows
+    // packet_startcode_prefix = 0x000001
+    // stream_id
+    // PES_packet_length = 0x????
+    // reserved = b10
+    // PES_scrambling_control = b00
+    // PES_priority = b0
+    // data_alignment_indicator = b1
+    // copyright = b0
+    // original_or_copy = b0
+    // PTS_DTS_flags = b10  (PTS only)
+    // ESCR_flag = b0
+    // ES_rate_flag = b0
+    // DSM_trick_mode_flag = b0
+    // additional_copy_info_flag = b0
+    // PES_CRC_flag = b0
+    // PES_extension_flag = b0
+    // PES_header_data_length = 0x05
+    // reserved = b0010 (PTS)
+    // PTS[32..30] = b???
+    // reserved = b1
+    // PTS[29..15] = b??? ???? ???? ???? (15 bits)
+    // reserved = b1
+    // PTS[14..0] = b??? ???? ???? ???? (15 bits)
+    // reserved = b1
+    // the first fragment of "buffer" follows
+
+    size_t numTSPackets;
+    if (accessUnit->size() <= 170) {
+        numTSPackets = 1;
+    } else {
+        numTSPackets = 1 + ((accessUnit->size() - 170) + 183) / 184;
+    }
+
+    if (flags & EMIT_PAT_AND_PMT) {
+        numTSPackets += 2;
+    }
+
+    if (flags & EMIT_PCR) {
+        ++numTSPackets;
+    }
+
+    sp<ABuffer> buffer = new ABuffer(numTSPackets * 188);
+    uint8_t *packetDataStart = buffer->data();
+
+    if (flags & EMIT_PAT_AND_PMT) {
+        // Program Association Table (PAT):
+        // 0x47
+        // transport_error_indicator = b0
+        // payload_unit_start_indicator = b1
+        // transport_priority = b0
+        // PID = b0000000000000 (13 bits)
+        // transport_scrambling_control = b00
+        // adaptation_field_control = b01 (no adaptation field, payload only)
+        // continuity_counter = b????
+        // skip = 0x00
+        // --- payload follows
+        // table_id = 0x00
+        // section_syntax_indicator = b1
+        // must_be_zero = b0
+        // reserved = b11
+        // section_length = 0x00d
+        // transport_stream_id = 0x0000
+        // reserved = b11
+        // version_number = b00001
+        // current_next_indicator = b1
+        // section_number = 0x00
+        // last_section_number = 0x00
+        //   one program follows:
+        //   program_number = 0x0001
+        //   reserved = b111
+        //   program_map_PID = kPID_PMT (13 bits!)
+        // CRC = 0x????????
+
+        if (++mPATContinuityCounter == 16) {
+            mPATContinuityCounter = 0;
+        }
+
+        uint8_t *ptr = packetDataStart;
+        *ptr++ = 0x47;
+        *ptr++ = 0x40;
+        *ptr++ = 0x00;
+        *ptr++ = 0x10 | mPATContinuityCounter;
+        *ptr++ = 0x00;
+
+        const uint8_t *crcDataStart = ptr;
+        *ptr++ = 0x00;
+        *ptr++ = 0xb0;
+        *ptr++ = 0x0d;
+        *ptr++ = 0x00;
+        *ptr++ = 0x00;
+        *ptr++ = 0xc3;
+        *ptr++ = 0x00;
+        *ptr++ = 0x00;
+        *ptr++ = 0x00;
+        *ptr++ = 0x01;
+        *ptr++ = 0xe0 | (kPID_PMT >> 8);
+        *ptr++ = kPID_PMT & 0xff;
+
+        CHECK_EQ(ptr - crcDataStart, 12);
+        uint32_t crc = htonl(crc32(crcDataStart, ptr - crcDataStart));
+        memcpy(ptr, &crc, 4);
+        ptr += 4;
+
+        size_t sizeLeft = packetDataStart + 188 - ptr;
+        memset(ptr, 0xff, sizeLeft);
+
+        packetDataStart += 188;
+
+        // Program Map (PMT):
+        // 0x47
+        // transport_error_indicator = b0
+        // payload_unit_start_indicator = b1
+        // transport_priority = b0
+        // PID = kPID_PMT (13 bits)
+        // transport_scrambling_control = b00
+        // adaptation_field_control = b01 (no adaptation field, payload only)
+        // continuity_counter = b????
+        // skip = 0x00
+        // -- payload follows
+        // table_id = 0x02
+        // section_syntax_indicator = b1
+        // must_be_zero = b0
+        // reserved = b11
+        // section_length = 0x???
+        // program_number = 0x0001
+        // reserved = b11
+        // version_number = b00001
+        // current_next_indicator = b1
+        // section_number = 0x00
+        // last_section_number = 0x00
+        // reserved = b111
+        // PCR_PID = kPCR_PID (13 bits)
+        // reserved = b1111
+        // program_info_length = 0x000
+        //   one or more elementary stream descriptions follow:
+        //   stream_type = 0x??
+        //   reserved = b111
+        //   elementary_PID = b? ???? ???? ???? (13 bits)
+        //   reserved = b1111
+        //   ES_info_length = 0x000
+        // CRC = 0x????????
+
+        if (++mPMTContinuityCounter == 16) {
+            mPMTContinuityCounter = 0;
+        }
+
+        size_t section_length = 5 * mTracks.size() + 4 + 9;
+
+        ptr = packetDataStart;
+        *ptr++ = 0x47;
+        *ptr++ = 0x40 | (kPID_PMT >> 8);
+        *ptr++ = kPID_PMT & 0xff;
+        *ptr++ = 0x10 | mPMTContinuityCounter;
+        *ptr++ = 0x00;
+
+        crcDataStart = ptr;
+        *ptr++ = 0x02;
+        *ptr++ = 0xb0 | (section_length >> 8);
+        *ptr++ = section_length & 0xff;
+        *ptr++ = 0x00;
+        *ptr++ = 0x01;
+        *ptr++ = 0xc3;
+        *ptr++ = 0x00;
+        *ptr++ = 0x00;
+        *ptr++ = 0xe0 | (kPID_PCR >> 8);
+        *ptr++ = kPID_PCR & 0xff;
+        *ptr++ = 0xf0;
+        *ptr++ = 0x00;
+
+        for (size_t i = 0; i < mTracks.size(); ++i) {
+            const sp<Track> &track = mTracks[i];
+
+            *ptr++ = track->streamType();
+            *ptr++ = 0xe0 | (track->PID() >> 8);
+            *ptr++ = track->PID() & 0xff;
+            *ptr++ = 0xf0;
+            *ptr++ = 0x00;
+        }
+
+        CHECK_EQ(static_cast<size_t>(ptr - crcDataStart),
+                 12 + mTracks.size() * 5);
+
+        crc = htonl(crc32(crcDataStart, ptr - crcDataStart));
+        memcpy(ptr, &crc, 4);
+        ptr += 4;
+
+        sizeLeft = packetDataStart + 188 - ptr;
+        memset(ptr, 0xff, sizeLeft);
+
+        packetDataStart += 188;
+    }
+
+    if (flags & EMIT_PCR) {
+        // PCR stream
+        // 0x47
+        // transport_error_indicator = b0
+        // payload_unit_start_indicator = b1
+        // transport_priority = b0
+        // PID = kPCR_PID (13 bits)
+        // transport_scrambling_control = b00
+        // adaptation_field_control = b10 (adaptation field only, no payload)
+        // continuity_counter = b0000 (does not increment)
+        // adaptation_field_length = 183
+        // discontinuity_indicator = b0
+        // random_access_indicator = b0
+        // elementary_stream_priority_indicator = b0
+        // PCR_flag = b1
+        // OPCR_flag = b0
+        // splicing_point_flag = b0
+        // transport_private_data_flag = b0
+        // adaptation_field_extension_flag = b0
+        // program_clock_reference_base = b?????????????????????????????????
+        // reserved = b111111
+        // program_clock_reference_extension = b?????????
+
+#if 0
+        int64_t nowUs = ALooper::GetNowUs();
+#else
+        int64_t nowUs = timeUs;
+#endif
+
+        uint64_t PCR = nowUs * 27;  // PCR based on a 27MHz clock
+        uint64_t PCR_base = PCR / 300;
+        uint32_t PCR_ext = PCR % 300;
+
+        uint8_t *ptr = packetDataStart;
+        *ptr++ = 0x47;
+        *ptr++ = 0x40 | (kPID_PCR >> 8);
+        *ptr++ = kPID_PCR & 0xff;
+        *ptr++ = 0x20;
+        *ptr++ = 0xb7;  // adaptation_field_length
+        *ptr++ = 0x10;
+        *ptr++ = (PCR_base >> 25) & 0xff;
+        *ptr++ = (PCR_base >> 17) & 0xff;
+        *ptr++ = (PCR_base >> 9) & 0xff;
+        *ptr++ = ((PCR_base & 1) << 7) | 0x7e | ((PCR_ext >> 8) & 1);
+        *ptr++ = (PCR_ext & 0xff);
+
+        size_t sizeLeft = packetDataStart + 188 - ptr;
+        memset(ptr, 0xff, sizeLeft);
+
+        packetDataStart += 188;
+    }
+
+    uint32_t PTS = static_cast<uint32_t>((timeUs * 9ll) / 100ll);
+
+    size_t PES_packet_length = accessUnit->size() + 8;
+    bool padding = (accessUnit->size() < (188 - 18));
+
+    if (PES_packet_length >= 65536) {
+        // This really should only happen for video.
+        CHECK(track->isVideo());
+
+        // It's valid to set this to 0 for video according to the specs.
+        PES_packet_length = 0;
+    }
+
+    uint8_t *ptr = packetDataStart;
+    *ptr++ = 0x47;
+    *ptr++ = 0x40 | (track->PID() >> 8);
+    *ptr++ = track->PID() & 0xff;
+    *ptr++ = (padding ? 0x30 : 0x10) | track->incrementContinuityCounter();
+
+    if (padding) {
+        size_t paddingSize = 188 - 18 - accessUnit->size();
+        *ptr++ = paddingSize - 1;
+        if (paddingSize >= 2) {
+            *ptr++ = 0x00;
+            memset(ptr, 0xff, paddingSize - 2);
+            ptr += paddingSize - 2;
+        }
+    }
+
+    *ptr++ = 0x00;
+    *ptr++ = 0x00;
+    *ptr++ = 0x01;
+    *ptr++ = track->streamID();
+    *ptr++ = PES_packet_length >> 8;
+    *ptr++ = PES_packet_length & 0xff;
+    *ptr++ = 0x84;
+    *ptr++ = 0x80;
+    *ptr++ = 0x05;
+    *ptr++ = 0x20 | (((PTS >> 30) & 7) << 1) | 1;
+    *ptr++ = (PTS >> 22) & 0xff;
+    *ptr++ = (((PTS >> 15) & 0x7f) << 1) | 1;
+    *ptr++ = (PTS >> 7) & 0xff;
+    *ptr++ = ((PTS & 0x7f) << 1) | 1;
+
+    // 18 bytes of TS/PES header leave 188 - 18 = 170 bytes for the payload
+
+    size_t sizeLeft = packetDataStart + 188 - ptr;
+    size_t copy = accessUnit->size();
+    if (copy > sizeLeft) {
+        copy = sizeLeft;
+    }
+
+    memcpy(ptr, accessUnit->data(), copy);
+    ptr += copy;
+    CHECK_EQ(sizeLeft, copy);
+    memset(ptr, 0xff, sizeLeft - copy);
+
+    packetDataStart += 188;
+
+    size_t offset = copy;
+    while (offset < accessUnit->size()) {
+        bool padding = (accessUnit->size() - offset) < (188 - 4);
+
+        // for subsequent fragments of "buffer":
+        // 0x47
+        // transport_error_indicator = b0
+        // payload_unit_start_indicator = b0
+        // transport_priority = b0
+        // PID = b0 0001 1110 ???? (13 bits) [0x1e0 + 1 + sourceIndex]
+        // transport_scrambling_control = b00
+        // adaptation_field_control = b??
+        // continuity_counter = b????
+        // the fragment of "buffer" follows.
+
+        uint8_t *ptr = packetDataStart;
+        *ptr++ = 0x47;
+        *ptr++ = 0x00 | (track->PID() >> 8);
+        *ptr++ = track->PID() & 0xff;
+
+        *ptr++ = (padding ? 0x30 : 0x10) | track->incrementContinuityCounter();
+
+        if (padding) {
+            size_t paddingSize = 188 - 4 - (accessUnit->size() - offset);
+            *ptr++ = paddingSize - 1;
+            if (paddingSize >= 2) {
+                *ptr++ = 0x00;
+                memset(ptr, 0xff, paddingSize - 2);
+                ptr += paddingSize - 2;
+            }
+        }
+
+        // 4 bytes of TS header leave 188 - 4 = 184 bytes for the payload
+
+        size_t sizeLeft = packetDataStart + 188 - ptr;
+        size_t copy = accessUnit->size() - offset;
+        if (copy > sizeLeft) {
+            copy = sizeLeft;
+        }
+
+        memcpy(ptr, accessUnit->data() + offset, copy);
+        ptr += copy;
+        CHECK_EQ(sizeLeft, copy);
+        memset(ptr, 0xff, sizeLeft - copy);
+
+        offset += copy;
+        packetDataStart += 188;
+    }
+
+    CHECK(packetDataStart == buffer->data() + buffer->capacity());
+
+    *packets = buffer;
+
+    return OK;
+}
+
+void TSPacketizer::initCrcTable() {
+    uint32_t poly = 0x04C11DB7;
+
+    for (int i = 0; i < 256; i++) {
+        uint32_t crc = i << 24;
+        for (int j = 0; j < 8; j++) {
+            crc = (crc << 1) ^ ((crc & 0x80000000) ? (poly) : 0);
+        }
+        mCrcTable[i] = crc;
+    }
+}
+
+uint32_t TSPacketizer::crc32(const uint8_t *start, size_t size) const {
+    uint32_t crc = 0xFFFFFFFF;
+    const uint8_t *p;
+
+    for (p = start; p < start + size; ++p) {
+        crc = (crc << 8) ^ mCrcTable[((crc >> 24) ^ *p) & 0xFF];
+    }
+
+    return crc;
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/Utils.cpp b/host/frontend/gcastv2/libandroid/Utils.cpp
new file mode 100644
index 0000000..750b95f
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/Utils.cpp
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+#include <arpa/inet.h>
+
+#include <media/stagefright/Utils.h>
+
+#include <media/stagefright/MetaData.h>
+
+namespace android {
+
+uint16_t U16_AT(const uint8_t *ptr) {
+    return ptr[0] << 8 | ptr[1];
+}
+
+uint32_t U32_AT(const uint8_t *ptr) {
+    return ptr[0] << 24 | ptr[1] << 16 | ptr[2] << 8 | ptr[3];
+}
+
+uint64_t U64_AT(const uint8_t *ptr) {
+    return ((uint64_t)U32_AT(ptr)) << 32 | U32_AT(ptr + 4);
+}
+
+uint16_t U16LE_AT(const uint8_t *ptr) {
+    return ptr[0] | (ptr[1] << 8);
+}
+
+uint32_t U32LE_AT(const uint8_t *ptr) {
+    return ptr[3] << 24 | ptr[2] << 16 | ptr[1] << 8 | ptr[0];
+}
+
+uint64_t U64LE_AT(const uint8_t *ptr) {
+    return ((uint64_t)U32LE_AT(ptr + 4)) << 32 | U32LE_AT(ptr);
+}
+
+// XXX warning: these won't work on big-endian host.
+uint64_t ntoh64(uint64_t x) {
+    return ((uint64_t)ntohl(x & 0xffffffff) << 32) | ntohl(x >> 32);
+}
+
+uint64_t hton64(uint64_t x) {
+    return ((uint64_t)htonl(x & 0xffffffff) << 32) | htonl(x >> 32);
+}
+
+status_t convertMetaDataToMessage(
+        const sp<MetaData> &meta, sp<AMessage> *format) {
+    format->clear();
+
+    const char *mime;
+    CHECK(meta->findCString(kKeyMIMEType, &mime));
+
+    sp<AMessage> msg = new AMessage;
+    msg->setString("mime", mime);
+
+    if (!strncasecmp("video/", mime, 6)) {
+        int32_t width, height;
+        CHECK(meta->findInt32(kKeyWidth, &width));
+        CHECK(meta->findInt32(kKeyHeight, &height));
+
+        msg->setInt32("width", width);
+        msg->setInt32("height", height);
+    } else if (!strncasecmp("audio/", mime, 6)) {
+        int32_t numChannels, sampleRate;
+        CHECK(meta->findInt32(kKeyChannelCount, &numChannels));
+        CHECK(meta->findInt32(kKeySampleRate, &sampleRate));
+
+        msg->setInt32("channel-count", numChannels);
+        msg->setInt32("sample-rate", sampleRate);
+
+        int32_t isADTS;
+        if (meta->findInt32(kKeyIsADTS, &isADTS)) {
+            msg->setInt32("is-adts", true);
+        }
+    }
+
+    uint32_t type;
+    const void *data;
+    size_t size;
+    if (meta->findData(kKeyAVCC, &type, &data, &size)) {
+        // Parse the AVCDecoderConfigurationRecord
+
+        const uint8_t *ptr = (const uint8_t *)data;
+
+        CHECK(size >= 7);
+        CHECK_EQ((unsigned)ptr[0], 1u);  // configurationVersion == 1
+        // uint8_t profile = ptr[1];
+        // uint8_t level = ptr[3];
+
+        // There is decodable content out there that fails the following
+        // assertion, let's be lenient for now...
+        // CHECK((ptr[4] >> 2) == 0x3f);  // reserved
+
+        // size_t lengthSize = 1 + (ptr[4] & 3);
+
+        // commented out check below as H264_QVGA_500_NO_AUDIO.3gp
+        // violates it...
+        // CHECK((ptr[5] >> 5) == 7);  // reserved
+
+        size_t numSeqParameterSets = ptr[5] & 31;
+
+        ptr += 6;
+        size -= 6;
+
+        sp<ABuffer> buffer = new ABuffer(1024);
+        buffer->setRange(0, 0);
+
+        for (size_t i = 0; i < numSeqParameterSets; ++i) {
+            CHECK(size >= 2);
+            size_t length = U16_AT(ptr);
+
+            ptr += 2;
+            size -= 2;
+
+            CHECK(size >= length);
+
+            memcpy(buffer->data() + buffer->size(), "\x00\x00\x00\x01", 4);
+            memcpy(buffer->data() + buffer->size() + 4, ptr, length);
+            buffer->setRange(0, buffer->size() + 4 + length);
+
+            ptr += length;
+            size -= length;
+        }
+
+        buffer->meta()->setInt32("csd", true);
+        buffer->meta()->setInt64("timeUs", 0);
+
+        msg->setBuffer("csd-0", buffer);
+
+        buffer = new ABuffer(1024);
+        buffer->setRange(0, 0);
+
+        CHECK(size >= 1);
+        size_t numPictureParameterSets = *ptr;
+        ++ptr;
+        --size;
+        for (size_t i = 0; i < numPictureParameterSets; ++i) {
+            CHECK(size >= 2);
+            size_t length = U16_AT(ptr);
+
+            ptr += 2;
+            size -= 2;
+
+            CHECK(size >= length);
+
+            memcpy(buffer->data() + buffer->size(), "\x00\x00\x00\x01", 4);
+            memcpy(buffer->data() + buffer->size() + 4, ptr, length);
+            buffer->setRange(0, buffer->size() + 4 + length);
+
+            ptr += length;
+            size -= length;
+        }
+
+        buffer->meta()->setInt32("csd", true);
+        buffer->meta()->setInt64("timeUs", 0);
+        msg->setBuffer("csd-1", buffer);
+    }
+
+    *format = msg;
+
+    return OK;
+}
+
+std::string MakeUserAgent() {
+    return "stagefright/1.2 (OS X)";
+}
+
+void toLower(std::string *s) {
+    std::transform(s->begin(), s->end(), s->begin(), ::tolower);
+}
+
+void trim(std::string *s) {
+    size_t i = 0;
+    while (i < s->size() && isspace(s->at(i))) {
+        ++i;
+    }
+
+    size_t j = s->size();
+    while (j > i && isspace(s->at(j - 1))) {
+        --j;
+    }
+
+    s->erase(0, i);
+    j -= i;
+    s->erase(j);
+}
+
+bool startsWith(std::string_view s1, std::string_view s2) {
+    if (s1.size() < s2.size()) {
+        return false;
+    }
+
+    return s1.substr(0, s2.size()) == s2;
+}
+
+std::string StringPrintf(const char *format, ...) {
+    va_list ap;
+    va_start(ap, format);
+
+    char *buffer;
+    (void)vasprintf(&buffer, format, ap);
+
+    va_end(ap);
+
+    std::string result(buffer);
+
+    free(buffer);
+    buffer = NULL;
+
+    return result;
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/avc_utils.cpp b/host/frontend/gcastv2/libandroid/avc_utils.cpp
new file mode 100644
index 0000000..860d045
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/avc_utils.cpp
@@ -0,0 +1,608 @@
+/*
+ * 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.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "avc_utils"
+#include <utils/Log.h>
+
+#include <media/stagefright/avc_utils.h>
+
+#include <media/stagefright/foundation/ABitReader.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/MediaDefs.h>
+#include <media/stagefright/MediaErrors.h>
+#include <media/stagefright/MetaData.h>
+
+namespace android {
+
+unsigned parseUE(ABitReader *br) {
+    unsigned numZeroes = 0;
+    while (br->getBits(1) == 0) {
+        ++numZeroes;
+    }
+
+    unsigned x = br->getBits(numZeroes);
+
+    return x + (1u << numZeroes) - 1;
+}
+
+// Determine video dimensions from the sequence parameterset.
+void FindAVCDimensions(
+        const sp<ABuffer> &seqParamSet, int32_t *width, int32_t *height) {
+    ABitReader br(seqParamSet->data() + 1, seqParamSet->size() - 1);
+
+    unsigned profile_idc = br.getBits(8);
+    br.skipBits(16);
+    parseUE(&br);  // seq_parameter_set_id
+
+    unsigned chroma_format_idc = 1;  // 4:2:0 chroma format
+
+    if (profile_idc == 100 || profile_idc == 110
+            || profile_idc == 122 || profile_idc == 244
+            || profile_idc == 44 || profile_idc == 83 || profile_idc == 86) {
+        chroma_format_idc = parseUE(&br);
+        if (chroma_format_idc == 3) {
+            br.skipBits(1);  // residual_colour_transform_flag
+        }
+        parseUE(&br);  // bit_depth_luma_minus8
+        parseUE(&br);  // bit_depth_chroma_minus8
+        br.skipBits(1);  // qpprime_y_zero_transform_bypass_flag
+        CHECK_EQ(br.getBits(1), 0u);  // seq_scaling_matrix_present_flag
+    }
+
+    parseUE(&br);  // log2_max_frame_num_minus4
+    unsigned pic_order_cnt_type = parseUE(&br);
+
+    if (pic_order_cnt_type == 0) {
+        parseUE(&br);  // log2_max_pic_order_cnt_lsb_minus4
+    } else if (pic_order_cnt_type == 1) {
+        // offset_for_non_ref_pic, offset_for_top_to_bottom_field and
+        // offset_for_ref_frame are technically se(v), but since we are
+        // just skipping over them the midpoint does not matter.
+
+        br.getBits(1);  // delta_pic_order_always_zero_flag
+        parseUE(&br);  // offset_for_non_ref_pic
+        parseUE(&br);  // offset_for_top_to_bottom_field
+
+        unsigned num_ref_frames_in_pic_order_cnt_cycle = parseUE(&br);
+        for (unsigned i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; ++i) {
+            parseUE(&br);  // offset_for_ref_frame
+        }
+    }
+
+    parseUE(&br);  // num_ref_frames
+    br.getBits(1);  // gaps_in_frame_num_value_allowed_flag
+
+    unsigned pic_width_in_mbs_minus1 = parseUE(&br);
+    unsigned pic_height_in_map_units_minus1 = parseUE(&br);
+    unsigned frame_mbs_only_flag = br.getBits(1);
+
+    *width = pic_width_in_mbs_minus1 * 16 + 16;
+
+    *height = (2 - frame_mbs_only_flag)
+        * (pic_height_in_map_units_minus1 * 16 + 16);
+
+    if (!frame_mbs_only_flag) {
+        br.getBits(1);  // mb_adaptive_frame_field_flag
+    }
+
+    br.getBits(1);  // direct_8x8_inference_flag
+
+    if (br.getBits(1)) {  // frame_cropping_flag
+        unsigned frame_crop_left_offset = parseUE(&br);
+        unsigned frame_crop_right_offset = parseUE(&br);
+        unsigned frame_crop_top_offset = parseUE(&br);
+        unsigned frame_crop_bottom_offset = parseUE(&br);
+
+        unsigned cropUnitX, cropUnitY;
+        if (chroma_format_idc == 0  /* monochrome */) {
+            cropUnitX = 1;
+            cropUnitY = 2 - frame_mbs_only_flag;
+        } else {
+            unsigned subWidthC = (chroma_format_idc == 3) ? 1 : 2;
+            unsigned subHeightC = (chroma_format_idc == 1) ? 2 : 1;
+
+            cropUnitX = subWidthC;
+            cropUnitY = subHeightC * (2 - frame_mbs_only_flag);
+        }
+
+        ALOGV("frame_crop = (%u, %u, %u, %u), cropUnitX = %u, cropUnitY = %u",
+             frame_crop_left_offset, frame_crop_right_offset,
+             frame_crop_top_offset, frame_crop_bottom_offset,
+             cropUnitX, cropUnitY);
+
+        *width -=
+            (frame_crop_left_offset + frame_crop_right_offset) * cropUnitX;
+        *height -=
+            (frame_crop_top_offset + frame_crop_bottom_offset) * cropUnitY;
+    }
+}
+
+status_t getNextNALUnit(
+        const uint8_t **_data, size_t *_size,
+        const uint8_t **nalStart, size_t *nalSize,
+        bool startCodeFollows) {
+    const uint8_t *data = *_data;
+    size_t size = *_size;
+
+    *nalStart = NULL;
+    *nalSize = 0;
+
+    if (size == 0) {
+        return -EAGAIN;
+    }
+
+    // Skip any number of leading 0x00.
+
+    size_t offset = 0;
+    while (offset < size && data[offset] == 0x00) {
+        ++offset;
+    }
+
+    if (offset == size) {
+        return -EAGAIN;
+    }
+
+    // A valid startcode consists of at least two 0x00 bytes followed by 0x01.
+
+    if (offset < 2 || data[offset] != 0x01) {
+        return ERROR_MALFORMED;
+    }
+
+    ++offset;
+
+    size_t startOffset = offset;
+
+    for (;;) {
+        while (offset < size && data[offset] != 0x01) {
+            ++offset;
+        }
+
+        if (offset == size) {
+            if (startCodeFollows) {
+                offset = size + 2;
+                break;
+            }
+
+            return -EAGAIN;
+        }
+
+        if (data[offset - 1] == 0x00 && data[offset - 2] == 0x00) {
+            break;
+        }
+
+        ++offset;
+    }
+
+    size_t endOffset = offset - 2;
+    while (data[endOffset - 1] == 0x00) {
+        --endOffset;
+    }
+
+    *nalStart = &data[startOffset];
+    *nalSize = endOffset - startOffset;
+
+    if (offset + 2 < size) {
+        *_data = &data[offset - 2];
+        *_size = size - offset + 2;
+    } else {
+        *_data = NULL;
+        *_size = 0;
+    }
+
+    return OK;
+}
+
+static sp<ABuffer> FindNAL(const uint8_t *data, size_t size, unsigned nalType) {
+    const uint8_t *nalStart;
+    size_t nalSize;
+    while (getNextNALUnit(&data, &size, &nalStart, &nalSize, true) == OK) {
+        if ((nalStart[0] & 0x1f) == nalType) {
+            sp<ABuffer> buffer = new ABuffer(nalSize);
+            memcpy(buffer->data(), nalStart, nalSize);
+            return buffer;
+        }
+    }
+
+    return NULL;
+}
+
+const char *AVCProfileToString(uint8_t profile) {
+    switch (profile) {
+        case kAVCProfileBaseline:
+            return "Baseline";
+        case kAVCProfileMain:
+            return "Main";
+        case kAVCProfileExtended:
+            return "Extended";
+        case kAVCProfileHigh:
+            return "High";
+        case kAVCProfileHigh10:
+            return "High 10";
+        case kAVCProfileHigh422:
+            return "High 422";
+        case kAVCProfileHigh444:
+            return "High 444";
+        case kAVCProfileCAVLC444Intra:
+            return "CAVLC 444 Intra";
+        default:   return "Unknown";
+    }
+}
+
+sp<MetaData> MakeAVCCodecSpecificData(const sp<ABuffer> &accessUnit) {
+    const uint8_t *data = accessUnit->data();
+    size_t size = accessUnit->size();
+
+    sp<ABuffer> seqParamSet = FindNAL(data, size, 7);
+    if (seqParamSet == NULL) {
+        return NULL;
+    }
+
+    int32_t width, height;
+    FindAVCDimensions(seqParamSet, &width, &height);
+
+    sp<ABuffer> picParamSet = FindNAL(data, size, 8);
+    CHECK(picParamSet != NULL);
+
+    size_t csdSize =
+        1 + 3 + 1 + 1
+        + 2 * 1 + seqParamSet->size()
+        + 1 + 2 * 1 + picParamSet->size();
+
+    sp<ABuffer> csd = new ABuffer(csdSize);
+    uint8_t *out = csd->data();
+
+    *out++ = 0x01;  // configurationVersion
+    memcpy(out, seqParamSet->data() + 1, 3);  // profile/level...
+
+    uint8_t profile = out[0];
+    uint8_t level = out[2];
+
+    out += 3;
+    *out++ = (0x3f << 2) | 1;  // lengthSize == 2 bytes
+    *out++ = 0xe0 | 1;
+
+    *out++ = seqParamSet->size() >> 8;
+    *out++ = seqParamSet->size() & 0xff;
+    memcpy(out, seqParamSet->data(), seqParamSet->size());
+    out += seqParamSet->size();
+
+    *out++ = 1;
+
+    *out++ = picParamSet->size() >> 8;
+    *out++ = picParamSet->size() & 0xff;
+    memcpy(out, picParamSet->data(), picParamSet->size());
+
+#if 0
+    ALOGI("AVC seq param set");
+    hexdump(seqParamSet->data(), seqParamSet->size());
+#endif
+
+    sp<MetaData> meta = new MetaData;
+    meta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_VIDEO_AVC);
+
+    meta->setData(kKeyAVCC, 0, csd->data(), csd->size());
+    meta->setInt32(kKeyWidth, width);
+    meta->setInt32(kKeyHeight, height);
+
+    ALOGI("found AVC codec config (%d x %d, %s-profile level %d.%d)",
+         width, height, AVCProfileToString(profile), level / 10, level % 10);
+
+    return meta;
+}
+
+bool IsIDR(const sp<ABuffer> &buffer) {
+    const uint8_t *data = buffer->data();
+    size_t size = buffer->size();
+
+    bool foundIDR = false;
+
+    const uint8_t *nalStart;
+    size_t nalSize;
+    while (getNextNALUnit(&data, &size, &nalStart, &nalSize, true) == OK) {
+        CHECK_GT(nalSize, 0u);
+
+        unsigned nalType = nalStart[0] & 0x1f;
+
+        if (nalType == 5) {
+            foundIDR = true;
+            break;
+        }
+    }
+
+    return foundIDR;
+}
+
+sp<MetaData> MakeAACCodecSpecificData(
+        unsigned profile, unsigned sampling_freq_index,
+        unsigned channel_configuration) {
+    sp<MetaData> meta = new MetaData;
+    meta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_AAC);
+
+    CHECK_LE(sampling_freq_index, 11u);
+    static const int32_t kSamplingFreq[] = {
+        96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050,
+        16000, 12000, 11025, 8000
+    };
+    meta->setInt32(kKeySampleRate, kSamplingFreq[sampling_freq_index]);
+    meta->setInt32(kKeyChannelCount, channel_configuration);
+
+    static const uint8_t kStaticESDS[] = {
+        0x03, 22,
+        0x00, 0x00,     // ES_ID
+        0x00,           // streamDependenceFlag, URL_Flag, OCRstreamFlag
+
+        0x04, 17,
+        0x40,                       // Audio ISO/IEC 14496-3
+        0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00,
+
+        0x05, 2,
+        // AudioSpecificInfo follows
+
+        // oooo offf fccc c000
+        // o - audioObjectType
+        // f - samplingFreqIndex
+        // c - channelConfig
+    };
+    sp<ABuffer> csd = new ABuffer(sizeof(kStaticESDS) + 2);
+    memcpy(csd->data(), kStaticESDS, sizeof(kStaticESDS));
+
+    csd->data()[sizeof(kStaticESDS)] =
+        ((profile + 1) << 3) | (sampling_freq_index >> 1);
+
+    csd->data()[sizeof(kStaticESDS) + 1] =
+        ((sampling_freq_index << 7) & 0x80) | (channel_configuration << 3);
+
+    meta->setData(kKeyESDS, 0, csd->data(), csd->size());
+
+    return meta;
+}
+
+bool ExtractDimensionsFromVOLHeader(
+        const uint8_t *data, size_t size, int32_t *width, int32_t *height) {
+    ABitReader br(&data[4], size - 4);
+    br.skipBits(1);  // random_accessible_vol
+    unsigned video_object_type_indication = br.getBits(8);
+
+    CHECK_NE(video_object_type_indication,
+             0x21u /* Fine Granularity Scalable */);
+
+    unsigned video_object_layer_verid;
+    unsigned video_object_layer_priority;
+    if (br.getBits(1)) {
+        video_object_layer_verid = br.getBits(4);
+        video_object_layer_priority = br.getBits(3);
+    }
+    unsigned aspect_ratio_info = br.getBits(4);
+    if (aspect_ratio_info == 0x0f /* extended PAR */) {
+        br.skipBits(8);  // par_width
+        br.skipBits(8);  // par_height
+    }
+    if (br.getBits(1)) {  // vol_control_parameters
+        br.skipBits(2);  // chroma_format
+        br.skipBits(1);  // low_delay
+        if (br.getBits(1)) {  // vbv_parameters
+            br.skipBits(15);  // first_half_bit_rate
+            CHECK(br.getBits(1));  // marker_bit
+            br.skipBits(15);  // latter_half_bit_rate
+            CHECK(br.getBits(1));  // marker_bit
+            br.skipBits(15);  // first_half_vbv_buffer_size
+            CHECK(br.getBits(1));  // marker_bit
+            br.skipBits(3);  // latter_half_vbv_buffer_size
+            br.skipBits(11);  // first_half_vbv_occupancy
+            CHECK(br.getBits(1));  // marker_bit
+            br.skipBits(15);  // latter_half_vbv_occupancy
+            CHECK(br.getBits(1));  // marker_bit
+        }
+    }
+    unsigned video_object_layer_shape = br.getBits(2);
+    CHECK_EQ(video_object_layer_shape, 0x00u /* rectangular */);
+
+    CHECK(br.getBits(1));  // marker_bit
+    unsigned vop_time_increment_resolution = br.getBits(16);
+    CHECK(br.getBits(1));  // marker_bit
+
+    if (br.getBits(1)) {  // fixed_vop_rate
+        // range [0..vop_time_increment_resolution)
+
+        // vop_time_increment_resolution
+        // 2 => 0..1, 1 bit
+        // 3 => 0..2, 2 bits
+        // 4 => 0..3, 2 bits
+        // 5 => 0..4, 3 bits
+        // ...
+
+        CHECK_GT(vop_time_increment_resolution, 0u);
+        --vop_time_increment_resolution;
+
+        unsigned numBits = 0;
+        while (vop_time_increment_resolution > 0) {
+            ++numBits;
+            vop_time_increment_resolution >>= 1;
+        }
+
+        br.skipBits(numBits);  // fixed_vop_time_increment
+    }
+
+    CHECK(br.getBits(1));  // marker_bit
+    unsigned video_object_layer_width = br.getBits(13);
+    CHECK(br.getBits(1));  // marker_bit
+    unsigned video_object_layer_height = br.getBits(13);
+    CHECK(br.getBits(1));  // marker_bit
+
+    // unsigned interlaced = br.getBits(1);
+
+    *width = video_object_layer_width;
+    *height = video_object_layer_height;
+
+    return true;
+}
+
+bool GetMPEGAudioFrameSize(
+        uint32_t header, size_t *frame_size,
+        int *out_sampling_rate, int *out_channels,
+        int *out_bitrate, int *out_num_samples) {
+    *frame_size = 0;
+
+    if (out_sampling_rate) {
+        *out_sampling_rate = 0;
+    }
+
+    if (out_channels) {
+        *out_channels = 0;
+    }
+
+    if (out_bitrate) {
+        *out_bitrate = 0;
+    }
+
+    if (out_num_samples) {
+        *out_num_samples = 1152;
+    }
+
+    if ((header & 0xffe00000) != 0xffe00000) {
+        return false;
+    }
+
+    unsigned version = (header >> 19) & 3;
+
+    if (version == 0x01) {
+        return false;
+    }
+
+    unsigned layer = (header >> 17) & 3;
+
+    if (layer == 0x00) {
+        return false;
+    }
+
+    // unsigned protection = (header >> 16) & 1;
+
+    unsigned bitrate_index = (header >> 12) & 0x0f;
+
+    if (bitrate_index == 0 || bitrate_index == 0x0f) {
+        // Disallow "free" bitrate.
+        return false;
+    }
+
+    unsigned sampling_rate_index = (header >> 10) & 3;
+
+    if (sampling_rate_index == 3) {
+        return false;
+    }
+
+    static const int kSamplingRateV1[] = { 44100, 48000, 32000 };
+    int sampling_rate = kSamplingRateV1[sampling_rate_index];
+    if (version == 2 /* V2 */) {
+        sampling_rate /= 2;
+    } else if (version == 0 /* V2.5 */) {
+        sampling_rate /= 4;
+    }
+
+    unsigned padding = (header >> 9) & 1;
+
+    if (layer == 3) {
+        // layer I
+
+        static const int kBitrateV1[] = {
+            32, 64, 96, 128, 160, 192, 224, 256,
+            288, 320, 352, 384, 416, 448
+        };
+
+        static const int kBitrateV2[] = {
+            32, 48, 56, 64, 80, 96, 112, 128,
+            144, 160, 176, 192, 224, 256
+        };
+
+        int bitrate =
+            (version == 3 /* V1 */)
+                ? kBitrateV1[bitrate_index - 1]
+                : kBitrateV2[bitrate_index - 1];
+
+        if (out_bitrate) {
+            *out_bitrate = bitrate;
+        }
+
+        *frame_size = (12000 * bitrate / sampling_rate + padding) * 4;
+
+        if (out_num_samples) {
+            *out_num_samples = 384;
+        }
+    } else {
+        // layer II or III
+
+        static const int kBitrateV1L2[] = {
+            32, 48, 56, 64, 80, 96, 112, 128,
+            160, 192, 224, 256, 320, 384
+        };
+
+        static const int kBitrateV1L3[] = {
+            32, 40, 48, 56, 64, 80, 96, 112,
+            128, 160, 192, 224, 256, 320
+        };
+
+        static const int kBitrateV2[] = {
+            8, 16, 24, 32, 40, 48, 56, 64,
+            80, 96, 112, 128, 144, 160
+        };
+
+        int bitrate;
+        if (version == 3 /* V1 */) {
+            bitrate = (layer == 2 /* L2 */)
+                ? kBitrateV1L2[bitrate_index - 1]
+                : kBitrateV1L3[bitrate_index - 1];
+
+            if (out_num_samples) {
+                *out_num_samples = 1152;
+            }
+        } else {
+            // V2 (or 2.5)
+
+            bitrate = kBitrateV2[bitrate_index - 1];
+            if (out_num_samples) {
+                *out_num_samples = 576;
+            }
+        }
+
+        if (out_bitrate) {
+            *out_bitrate = bitrate;
+        }
+
+        if (version == 3 /* V1 */) {
+            *frame_size = 144000 * bitrate / sampling_rate + padding;
+        } else {
+            // V2 or V2.5
+            *frame_size = 72000 * bitrate / sampling_rate + padding;
+        }
+    }
+
+    if (out_sampling_rate) {
+        *out_sampling_rate = sampling_rate;
+    }
+
+    if (out_channels) {
+        int channel_mode = (header >> 6) & 3;
+
+        *out_channels = (channel_mode == 3) ? 1 : 2;
+    }
+
+    return true;
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/base64.cpp b/host/frontend/gcastv2/libandroid/base64.cpp
new file mode 100644
index 0000000..3ec5410
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/base64.cpp
@@ -0,0 +1,122 @@
+#include <media/stagefright/foundation/base64.h>
+
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/ADebug.h>
+
+namespace android {
+
+sp<ABuffer> decodeBase64(const std::string_view &s) {
+    if ((s.size() % 4) != 0) {
+        return NULL;
+    }
+
+    size_t n = s.size();
+    size_t padding = 0;
+    if (n >= 1 && s[n - 1] == '=') {
+        padding = 1;
+
+        if (n >= 2 && s[n - 2] == '=') {
+            padding = 2;
+        }
+    }
+
+    size_t outLen = 3 * s.size() / 4 - padding;
+
+    sp<ABuffer> buffer = new ABuffer(outLen);
+
+    uint8_t *out = buffer->data();
+    size_t j = 0;
+    uint32_t accum = 0;
+    for (size_t i = 0; i < n; ++i) {
+        char c = s[i];
+        unsigned value;
+        if (c >= 'A' && c <= 'Z') {
+            value = c - 'A';
+        } else if (c >= 'a' && c <= 'z') {
+            value = 26 + c - 'a';
+        } else if (c >= '0' && c <= '9') {
+            value = 52 + c - '0';
+        } else if (c == '+') {
+            value = 62;
+        } else if (c == '/') {
+            value = 63;
+        } else if (c != '=') {
+            return NULL;
+        } else {
+            if (i < n - padding) {
+                return NULL;
+            }
+
+            value = 0;
+        }
+
+        accum = (accum << 6) | value;
+
+        if (((i + 1) % 4) == 0) {
+            out[j++] = (accum >> 16);
+
+            if (j < outLen) { out[j++] = (accum >> 8) & 0xff; }
+            if (j < outLen) { out[j++] = accum & 0xff; }
+
+            accum = 0;
+        }
+    }
+
+    return buffer;
+}
+
+static char encode6Bit(unsigned x) {
+    if (x <= 25) {
+        return 'A' + x;
+    } else if (x <= 51) {
+        return 'a' + x - 26;
+    } else if (x <= 61) {
+        return '0' + x - 52;
+    } else if (x == 62) {
+        return '+';
+    } else {
+        return '/';
+    }
+}
+
+void encodeBase64(const void *_data, size_t size, std::string *out) {
+    out->clear();
+
+    const uint8_t *data = (const uint8_t *)_data;
+
+    size_t i;
+    for (i = 0; i < (size / 3) * 3; i += 3) {
+        uint8_t x1 = data[i];
+        uint8_t x2 = data[i + 1];
+        uint8_t x3 = data[i + 2];
+
+        out->append(1, encode6Bit(x1 >> 2));
+        out->append(1, encode6Bit((x1 << 4 | x2 >> 4) & 0x3f));
+        out->append(1, encode6Bit((x2 << 2 | x3 >> 6) & 0x3f));
+        out->append(1, encode6Bit(x3 & 0x3f));
+    }
+    switch (size % 3) {
+        case 0:
+            break;
+        case 2:
+        {
+            uint8_t x1 = data[i];
+            uint8_t x2 = data[i + 1];
+            out->append(1, encode6Bit(x1 >> 2));
+            out->append(1, encode6Bit((x1 << 4 | x2 >> 4) & 0x3f));
+            out->append(1, encode6Bit((x2 << 2) & 0x3f));
+            out->append(1, '=');
+            break;
+        }
+        default:
+        {
+            uint8_t x1 = data[i];
+            out->append(1, encode6Bit(x1 >> 2));
+            out->append(1, encode6Bit((x1 << 4) & 0x3f));
+            out->append("==");
+            break;
+        }
+    }
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/hexdump.cpp b/host/frontend/gcastv2/libandroid/hexdump.cpp
new file mode 100644
index 0000000..85b57b2
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/hexdump.cpp
@@ -0,0 +1,73 @@
+#include <media/stagefright/foundation/hexdump.h>
+
+#include <media/stagefright/foundation/ADebug.h>
+
+#include <ctype.h>
+#include <stdint.h>
+#include <stdio.h>
+
+namespace android {
+
+static void appendIndent(std::string *s, int32_t indent) {
+    static const char kWhitespace[] =
+        "                                        "
+        "                                        ";
+
+    CHECK_LT((size_t)indent, sizeof(kWhitespace));
+
+    s->append(kWhitespace, indent);
+}
+
+void hexdump(const void *_data, size_t size, size_t indent, std::string *appendTo) {
+    const uint8_t *data = (const uint8_t *)_data;
+
+    size_t offset = 0;
+    while (offset < size) {
+        std::string line;
+
+        appendIndent(&line, indent);
+
+        char tmp[32];
+        snprintf(tmp, sizeof(tmp), "%08lx:  ", (unsigned long)offset);
+
+        line.append(tmp);
+
+        for (size_t i = 0; i < 16; ++i) {
+            if (i == 8) {
+                line.append(1, ' ');
+            }
+            if (offset + i >= size) {
+                line.append("   ");
+            } else {
+                snprintf(tmp, sizeof(tmp), "%02x ", data[offset + i]);
+                line.append(tmp);
+            }
+        }
+
+        line.append(1, ' ');
+
+        for (size_t i = 0; i < 16; ++i) {
+            if (offset + i >= size) {
+                break;
+            }
+
+            if (isprint(data[offset + i])) {
+                line.append(1, (char)data[offset + i]);
+            } else {
+                line.append(1, '.');
+            }
+        }
+
+        if (appendTo != NULL) {
+            appendTo->append(line);
+            appendTo->append("\n");
+        } else {
+            ALOGI("%s", line.c_str());
+        }
+
+        offset += 16;
+    }
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/include/helpers/JavaThread.h b/host/frontend/gcastv2/libandroid/include/helpers/JavaThread.h
new file mode 100644
index 0000000..4694dd4
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/helpers/JavaThread.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <helpers/MyAndroidRuntime.h>
+
+#include <thread>
+
+namespace android {
+
+void javaAttachThread();
+void javaDetachThread();
+
+template<class Function, class... Args>
+std::thread createJavaThread(Function &&f, Args&&... args) {
+    return std::thread([f, args...]{
+        javaAttachThread();
+        f(args...);
+        javaDetachThread();
+    });
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/include/helpers/MyAndroidRuntime.h b/host/frontend/gcastv2/libandroid/include/helpers/MyAndroidRuntime.h
new file mode 100644
index 0000000..e1aae88
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/helpers/MyAndroidRuntime.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <jni.h>
+
+namespace android {
+
+struct MyAndroidRuntime {
+    static void setJavaVM(JavaVM *vm);
+    static JavaVM *getJavaVM();
+
+    static JNIEnv *getJNIEnv();
+};
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/include/helpers/MyJNIHelpers.h b/host/frontend/gcastv2/libandroid/include/helpers/MyJNIHelpers.h
new file mode 100644
index 0000000..0d6c9c8
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/helpers/MyJNIHelpers.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <jni.h>
+#include <sys/types.h>
+
+namespace android {
+
+void jniThrowException(
+        JNIEnv *env, const char *className, const char *msg);
+
+int jniRegisterNativeMethods(
+        JNIEnv *env,
+        const char *className,
+        const JNINativeMethod *methods,
+        size_t numMethods);
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/include/helpers/MyScopedByteArray.h b/host/frontend/gcastv2/libandroid/include/helpers/MyScopedByteArray.h
new file mode 100644
index 0000000..62fc607
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/helpers/MyScopedByteArray.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <jni.h>
+
+namespace android {
+
+struct MyScopedByteArray {
+    explicit MyScopedByteArray(JNIEnv *env, jbyteArray arrayObj);
+
+    MyScopedByteArray(const MyScopedByteArray &) = delete;
+    MyScopedByteArray &operator=(const MyScopedByteArray &) = delete;
+
+    ~MyScopedByteArray();
+
+    const jbyte *data() const;
+    jsize size() const;
+
+private:
+    JNIEnv *mEnv;
+    jbyteArray mArrayObj;
+    jbyte *mElements;
+    jsize mSize;
+};
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/include/helpers/MyScopedLocalRef.h b/host/frontend/gcastv2/libandroid/include/helpers/MyScopedLocalRef.h
new file mode 100644
index 0000000..d683669
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/helpers/MyScopedLocalRef.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <jni.h>
+
+template<class T>
+struct MyScopedLocalRef {
+    explicit MyScopedLocalRef()
+        : mEnv(nullptr),
+          mObj(nullptr) {
+    }
+
+    explicit MyScopedLocalRef(JNIEnv *env, T obj)
+        : mEnv(env),
+          mObj(obj) {
+    }
+
+    MyScopedLocalRef(const MyScopedLocalRef<T> &other) = delete;
+    MyScopedLocalRef &operator=(const MyScopedLocalRef<T> &other) = delete;
+
+    void setTo(JNIEnv *env, T obj) {
+        if (obj != mObj) {
+            clear();
+
+            mEnv = env;
+            mObj = obj;
+        }
+    }
+
+    void clear() {
+        if (mObj) {
+            mEnv->DeleteLocalRef(mObj);
+            mObj = nullptr;
+        }
+
+        mEnv = nullptr;
+    }
+
+    ~MyScopedLocalRef() {
+        clear();
+    }
+
+    T get() const {
+        return mObj;
+    }
+
+private:
+    JNIEnv *mEnv;
+    T mObj;
+};
diff --git a/host/frontend/gcastv2/libandroid/include/helpers/MyScopedUTF8String.h b/host/frontend/gcastv2/libandroid/include/helpers/MyScopedUTF8String.h
new file mode 100644
index 0000000..49e74e6
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/helpers/MyScopedUTF8String.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <jni.h>
+
+namespace android {
+
+struct MyScopedUTF8String {
+    explicit MyScopedUTF8String(JNIEnv *env, jstring stringObj);
+
+    MyScopedUTF8String(const MyScopedUTF8String &) = delete;
+    MyScopedUTF8String &operator=(const MyScopedUTF8String &) = delete;
+
+    ~MyScopedUTF8String();
+
+    const char *c_str() const;
+
+private:
+    JNIEnv *mEnv;
+    jstring mStringObj;
+    const char *mData;
+};
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/ATSParser.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/ATSParser.h
new file mode 100644
index 0000000..3d53fdb
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/ATSParser.h
@@ -0,0 +1,142 @@
+/*
+ * 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 A_TS_PARSER_H_
+
+#define A_TS_PARSER_H_
+
+#include <sys/types.h>
+
+#include <media/stagefright/foundation/ABase.h>
+#include <media/stagefright/foundation/AMessage.h>
+#include <utils/RefBase.h>
+
+#include <map>
+#include <vector>
+
+namespace android {
+
+struct ABitReader;
+struct ABuffer;
+struct AnotherPacketSource;
+
+struct ATSParser : public RefBase {
+    enum DiscontinuityType {
+        DISCONTINUITY_NONE              = 0,
+        DISCONTINUITY_TIME              = 1,
+        DISCONTINUITY_AUDIO_FORMAT      = 2,
+        DISCONTINUITY_VIDEO_FORMAT      = 4,
+        DISCONTINUITY_ABSOLUTE_TIME     = 8,
+        DISCONTINUITY_TIME_OFFSET       = 16,
+
+        DISCONTINUITY_SEEK              = DISCONTINUITY_TIME,
+
+        // For legacy reasons this also implies a time discontinuity.
+        DISCONTINUITY_FORMATCHANGE      =
+            DISCONTINUITY_AUDIO_FORMAT
+                | DISCONTINUITY_VIDEO_FORMAT
+                | DISCONTINUITY_TIME,
+    };
+
+    enum Flags {
+        // The 90kHz clock (PTS/DTS) is absolute, i.e. PTS=0 corresponds to
+        // a media time of 0.
+        // If this flag is _not_ specified, the first PTS encountered in a
+        // program of this stream will be assumed to correspond to media time 0
+        // instead.
+        TS_TIMESTAMPS_ARE_ABSOLUTE = 1,
+        // Video PES packets contain exactly one (aligned) access unit.
+        ALIGNED_VIDEO_DATA         = 2,
+
+        DUMP_PTS                   = 4,
+    };
+
+    ATSParser(uint32_t flags = 0);
+
+    status_t feedTSPacket(const void *data, size_t size);
+
+    void signalDiscontinuity(
+            DiscontinuityType type, const sp<AMessage> &extra);
+
+    void signalEOS(status_t finalResult);
+
+    enum SourceType {
+        VIDEO,
+        AUDIO
+    };
+    sp<AnotherPacketSource> getSource(SourceType type);
+
+    bool PTSTimeDeltaEstablished();
+
+    enum {
+        // From ISO/IEC 13818-1: 2000 (E), Table 2-29
+        STREAMTYPE_RESERVED             = 0x00,
+        STREAMTYPE_MPEG1_VIDEO          = 0x01,
+        STREAMTYPE_MPEG2_VIDEO          = 0x02,
+        STREAMTYPE_MPEG1_AUDIO          = 0x03,
+        STREAMTYPE_MPEG2_AUDIO          = 0x04,
+        STREAMTYPE_MPEG2_AUDIO_ADTS     = 0x0f,
+        STREAMTYPE_MPEG4_VIDEO          = 0x10,
+        STREAMTYPE_H264                 = 0x1b,
+        STREAMTYPE_PCM_AUDIO            = 0x83,
+    };
+
+protected:
+    virtual ~ATSParser();
+
+private:
+    struct Program;
+    struct Stream;
+    struct PSISection;
+
+    uint32_t mFlags;
+    std::vector<sp<Program>> mPrograms;
+
+    // Keyed by PID
+    std::map<unsigned, sp<PSISection> > mPSISections;
+
+    int64_t mAbsoluteTimeAnchorUs;
+
+    bool mTimeOffsetValid;
+    int64_t mTimeOffsetUs;
+
+    size_t mNumTSPacketsParsed;
+
+    void parseProgramAssociationTable(ABitReader *br);
+    void parseProgramMap(ABitReader *br);
+    void parsePES(ABitReader *br);
+
+    status_t parsePID(
+        ABitReader *br, unsigned PID,
+        unsigned continuity_counter,
+        unsigned payload_unit_start_indicator);
+
+    void parseAdaptationField(ABitReader *br, unsigned PID);
+    status_t parseTS(ABitReader *br);
+
+    void updatePCR(unsigned PID, uint64_t PCR, size_t byteOffsetFromStart);
+
+    uint64_t mPCR[2];
+    size_t mPCRBytes[2];
+    int64_t mSystemTimeUs[2];
+    size_t mNumPCRs;
+
+    DISALLOW_EVIL_CONSTRUCTORS(ATSParser);
+};
+
+}  // namespace android
+
+#endif  // A_TS_PARSER_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/AnotherPacketSource.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/AnotherPacketSource.h
new file mode 100644
index 0000000..5f9c1fc
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/AnotherPacketSource.h
@@ -0,0 +1,84 @@
+/*
+ * 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 ANOTHER_PACKET_SOURCE_H_
+
+#define ANOTHER_PACKET_SOURCE_H_
+
+#include <media/stagefright/foundation/ABase.h>
+#include <media/stagefright/ATSParser.h>
+#include <media/stagefright/MediaSource.h>
+#include <utils/threads.h>
+
+#include <list>
+
+namespace android {
+
+struct ABuffer;
+
+struct AnotherPacketSource : public MediaSource {
+    AnotherPacketSource(const sp<MetaData> &meta);
+
+    void setFormat(const sp<MetaData> &meta);
+
+    virtual status_t start(MetaData *params = NULL);
+    virtual status_t stop();
+    virtual sp<MetaData> getFormat();
+
+    virtual status_t read(
+            MediaBuffer **buffer, const ReadOptions *options = NULL);
+
+    bool hasBufferAvailable(status_t *finalResult);
+
+    // Returns the difference between the last and the first queued
+    // presentation timestamps since the last discontinuity (if any).
+    int64_t getBufferedDurationUs(status_t *finalResult);
+
+    status_t nextBufferTime(int64_t *timeUs);
+
+    void queueAccessUnit(const sp<ABuffer> &buffer);
+
+    void queueDiscontinuity(
+            ATSParser::DiscontinuityType type, const sp<AMessage> &extra);
+
+    void signalEOS(status_t result);
+
+    status_t dequeueAccessUnit(sp<ABuffer> *buffer);
+
+    bool isFinished(int64_t duration) const;
+
+protected:
+    virtual ~AnotherPacketSource();
+
+private:
+    Mutex mLock;
+    Condition mCondition;
+
+    bool mIsAudio;
+    sp<MetaData> mFormat;
+    int64_t mLastQueuedTimeUs;
+    std::list<sp<ABuffer>> mBuffers;
+    status_t mEOSResult;
+
+    bool wasFormatChange(int32_t discontinuityType) const;
+
+    DISALLOW_EVIL_CONSTRUCTORS(AnotherPacketSource);
+};
+
+
+}  // namespace android
+
+#endif  // ANOTHER_PACKET_SOURCE_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/FileSource.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/FileSource.h
new file mode 100644
index 0000000..2f4a739
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/FileSource.h
@@ -0,0 +1,30 @@
+#ifndef FILE_SOURCE_H_
+
+#define FILE_SOURCE_H_
+
+#include <utils/Errors.h>
+#include <utils/RefBase.h>
+
+namespace android {
+
+struct FileSource : public RefBase {
+    FileSource(const char *path);
+
+    status_t initCheck() const;
+
+    status_t getSize(off_t *size) const;
+    ssize_t readAt(off_t offset, void *data, size_t size);
+
+protected:
+    virtual ~FileSource();
+
+private:
+    int mFd;
+    status_t mInitCheck;
+
+    DISALLOW_EVIL_CONSTRUCTORS(FileSource);
+};
+
+}  // namespace android
+
+#endif  // FILE_SOURCE_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/MediaBuffer.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/MediaBuffer.h
new file mode 100644
index 0000000..a81c8f4
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/MediaBuffer.h
@@ -0,0 +1,37 @@
+#ifndef ANDROID_MEDIA_BUFFER_H_
+
+#define ANDROID_MEDIA_BUFFER_H_
+
+#include <media/stagefright/MetaData.h>
+
+namespace android {
+
+struct ABuffer;
+
+struct MediaBuffer {
+    MediaBuffer(size_t size)
+        : mBuffer(new ABuffer(size)) {
+    }
+
+    sp<MetaData> meta_data() {
+        if (mMeta == NULL) {
+            mMeta = new MetaData;
+        }
+
+        return mMeta;
+    }
+
+    void *data() {
+        return mBuffer->data();
+    }
+
+private:
+    sp<ABuffer> mBuffer;
+    sp<MetaData> mMeta;
+
+    DISALLOW_EVIL_CONSTRUCTORS(MediaBuffer);
+};
+
+}  // namespace android
+
+#endif  // ANDROID_MEDIA_BUFFER_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/MediaDefs.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/MediaDefs.h
new file mode 100644
index 0000000..9492752
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/MediaDefs.h
@@ -0,0 +1,17 @@
+#ifndef ANDROID_MEDIA_DEFS_H_
+
+#define ANDROID_MEDIA_DEFS_H_
+
+#define MEDIA_MIMETYPE_VIDEO_AVC        "video/avc"
+#define MEDIA_MIMETYPE_AUDIO_AAC        "audio/mp4a-latm"
+#define MEDIA_MIMETYPE_AUDIO_MPEG       "audio/mpeg"
+#define MEDIA_MIMETYPE_VIDEO_MPEG2      "video/mpeg2"
+#define MEDIA_MIMETYPE_VIDEO_MPEG4      "video/mp4v-es"
+#define MEDIA_MIMETYPE_VIDEO_VPX        "video/x-vnd.on2.vpx"
+#define MEDIA_MIMETYPE_VIDEO_VP8        "video/x-vnd.on2.vp8"
+#define MEDIA_MIMETYPE_AUDIO_RAW        "audio/raw"
+#define MEDIA_MIMETYPE_AUDIO_MPEG_LAYER_I       "audio/mpegL1"
+#define MEDIA_MIMETYPE_AUDIO_MPEG_LAYER_II       "audio/mpegL2"
+#define MEDIA_MIMETYPE_AUDIO_OPUS       "audio/opus"
+
+#endif  // ANDROID_MEDIA_DEFS_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/MediaErrors.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/MediaErrors.h
new file mode 100644
index 0000000..37d7dc3
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/MediaErrors.h
@@ -0,0 +1,15 @@
+#ifndef ANDROID_MEDIA_ERRORS_H_
+
+#define ANDROID_MEDIA_ERRORS_H_
+
+namespace android {
+
+enum {
+    ERROR_END_OF_STREAM = -10000,
+    ERROR_MALFORMED,
+    INFO_DISCONTINUITY,
+};
+
+}  // namespace android
+
+#endif  // ANDROID_MEDIA_ERRORS_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/MediaSource.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/MediaSource.h
new file mode 100644
index 0000000..28f9329
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/MediaSource.h
@@ -0,0 +1,35 @@
+#ifndef ANDROID_MEDIASOURCE_H_
+
+#define ANDROID_MEDIASOURCE_H_
+
+#include <utils/Errors.h>
+#include <utils/RefBase.h>
+
+namespace android {
+
+struct MediaBuffer;
+struct MetaData;
+
+struct MediaSource : public RefBase {
+    struct ReadOptions {
+    };
+
+    MediaSource() {}
+
+    virtual status_t start(MetaData *params = NULL) = 0;
+    virtual status_t stop() = 0;
+    virtual sp<MetaData> getFormat() = 0;
+
+    virtual status_t read(
+            MediaBuffer **out, const ReadOptions *params = NULL) = 0;
+
+protected:
+    virtual ~MediaSource() {}
+
+private:
+    DISALLOW_EVIL_CONSTRUCTORS(MediaSource);
+};
+
+}  // namespace android
+
+#endif  // ANDROID_MEDIASOURCE_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/MetaData.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/MetaData.h
new file mode 100644
index 0000000..fde2308
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/MetaData.h
@@ -0,0 +1,115 @@
+#ifndef ANDROID_METADATA_H_
+
+#define ANDROID_METADATA_H_
+
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/AMessage.h>
+#include <utils/RefBase.h>
+
+namespace android {
+
+enum {
+    kKeyMIMEType        = 'mime',
+    kKeyWidth           = 'widt',
+    kKeyHeight          = 'heig',
+    kKeyDuration        = 'dura',
+    kKeyAVCC            = 'avcc',
+    kKeyESDS            = 'esds',
+    kKeyTime            = 'time',
+    kKeySampleRate      = 'srat',
+    kKeyChannelCount    = '#chn',
+    kKeyIsADTS          = 'adts',
+};
+
+enum {
+  kTypeESDS = 'esds'
+};
+
+struct MetaData : public RefBase {
+    MetaData()
+        : mMessage(new AMessage) {
+    }
+
+    void setInt32(uint32_t key, int32_t x) {
+        std::string tmp;
+        tmp.append(std::to_string(key));
+
+        mMessage->setInt32(tmp.c_str(), x);
+    }
+
+    void setInt64(uint32_t key, int64_t x) {
+        std::string tmp;
+        tmp.append(std::to_string(key));
+
+        mMessage->setInt64(tmp.c_str(), x);
+    }
+
+    bool findInt32(uint32_t key, int32_t *x) const {
+        std::string tmp;
+      tmp.append(std::to_string(key));
+
+      return mMessage->findInt32(tmp.c_str(), x);
+    }
+
+    bool findCString(uint32_t key, const char **x) const {
+        std::string tmp;
+      tmp.append(std::to_string(key));
+
+      static std::string value;
+      if (!mMessage->findString(tmp.c_str(), &value)) {
+          *x = NULL;
+          return false;
+      }
+
+      *x = value.c_str();
+      return true;
+    }
+
+    void setCString(uint32_t key, const char *s) {
+        std::string tmp;
+        tmp.append(std::to_string(key));
+
+        mMessage->setString(tmp.c_str(), s);
+    }
+
+    void setData(uint32_t key, uint32_t type, const void *data, size_t size) {
+        std::string tmp;
+        tmp.append(std::to_string(key));
+
+        sp<ABuffer> buffer = new ABuffer(size);
+        buffer->meta()->setInt32("type", type);
+        memcpy(buffer->data(), data, size);
+
+        mMessage->setObject(tmp.c_str(), buffer);
+    }
+
+    bool findData(
+            uint32_t key, uint32_t *type, const void **data, size_t *size) const {
+        std::string tmp;
+        tmp.append(std::to_string(key));
+
+        sp<RefBase> obj;
+        if (!mMessage->findObject(tmp.c_str(), &obj)) {
+            return false;
+        }
+
+        sp<ABuffer> buffer = static_cast<ABuffer *>(obj.get());
+        CHECK(buffer->meta()->findInt32("type", (int32_t *)type));
+        *data = buffer->data();
+        *size = buffer->size();
+
+        return true;
+    }
+
+protected:
+    virtual ~MetaData() {}
+
+private:
+    sp<AMessage> mMessage;
+
+    DISALLOW_EVIL_CONSTRUCTORS(MetaData);
+};
+
+}  // namespace android
+
+#endif  // ANDROID_METADATA_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/NuMediaExtractor.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/NuMediaExtractor.h
new file mode 100644
index 0000000..97ff1f5
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/NuMediaExtractor.h
@@ -0,0 +1,64 @@
+#ifndef NU_MEDIA_EXTRACTOR_H_
+
+#define NU_MEDIA_EXTRACTOR_H_
+
+#include <stdio.h>
+
+#include <utils/Errors.h>
+#include <utils/RefBase.h>
+
+namespace android {
+
+struct ABuffer;
+struct AMessage;
+struct AnotherPacketSource;
+struct ATSParser;
+
+struct NuMediaExtractor : public RefBase {
+    NuMediaExtractor();
+
+    status_t setDataSource(const char *path);
+
+    size_t countTracks() const;
+    status_t getTrackFormat(size_t index, sp<AMessage> *format) const;
+
+    status_t selectTrack(size_t index);
+
+    status_t getSampleTime(int64_t *timeUs);
+    status_t getSampleTrackIndex(size_t *index);
+    status_t readSampleData(sp<ABuffer> accessUnit);
+
+    status_t advance();
+
+protected:
+    virtual ~NuMediaExtractor();
+
+private:
+    enum Flags {
+        FLAG_AUDIO_SELECTED = 1,
+        FLAG_VIDEO_SELECTED = 2,
+    };
+
+    uint32_t mFlags;
+    sp<ATSParser> mParser;
+    FILE *mFile;
+
+    sp<AnotherPacketSource> mAudioSource;
+    sp<AnotherPacketSource> mVideoSource;
+    size_t mNumTracks;
+    ssize_t mAudioTrackIndex;
+    ssize_t mVideoTrackIndex;
+
+    sp<ABuffer> mNextBuffer[2];
+    status_t mFinalResult[2];
+    ssize_t mNextIndex;
+
+    status_t feedMoreData();
+    void fetchSamples();
+
+    DISALLOW_EVIL_CONSTRUCTORS(NuMediaExtractor);
+};
+
+}  // namespace android
+
+#endif // NU_MEDIA_EXTRACTOR_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/Utils.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/Utils.h
new file mode 100644
index 0000000..4ae04b1
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/Utils.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2009 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 UTILS_H_
+
+#define UTILS_H_
+
+#include <stdint.h>
+#include <utils/Errors.h>
+#include <utils/RefBase.h>
+
+#include <string>
+#include <string_view>
+
+namespace android {
+
+#define FOURCC(c1, c2, c3, c4) \
+    (c1 << 24 | c2 << 16 | c3 << 8 | c4)
+
+uint16_t U16_AT(const uint8_t *ptr);
+uint32_t U32_AT(const uint8_t *ptr);
+uint64_t U64_AT(const uint8_t *ptr);
+
+uint16_t U16LE_AT(const uint8_t *ptr);
+uint32_t U32LE_AT(const uint8_t *ptr);
+uint64_t U64LE_AT(const uint8_t *ptr);
+
+uint64_t ntoh64(uint64_t x);
+uint64_t hton64(uint64_t x);
+
+struct MetaData;
+struct AMessage;
+status_t convertMetaDataToMessage(
+                const sp<MetaData> &meta, sp<AMessage> *format);
+void convertMessageToMetaData(
+                const sp<AMessage> &format, sp<MetaData> &meta);
+
+std::string MakeUserAgent();
+
+void toLower(std::string *s);
+void trim(std::string *s);
+bool startsWith(std::string_view s1, std::string_view s2);
+
+std::string StringPrintf(const char *format, ...);
+
+}  // namespace android
+
+#endif  // UTILS_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/avc_utils.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/avc_utils.h
new file mode 100644
index 0000000..15cd4d4
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/avc_utils.h
@@ -0,0 +1,72 @@
+/*
+ * 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 AVC_UTILS_H_
+
+#define AVC_UTILS_H_
+
+#include <media/stagefright/foundation/ABuffer.h>
+#include <utils/Errors.h>
+
+namespace android {
+
+struct ABitReader;
+
+enum {
+    kAVCProfileBaseline      = 0x42,
+    kAVCProfileMain          = 0x4d,
+    kAVCProfileExtended      = 0x58,
+    kAVCProfileHigh          = 0x64,
+    kAVCProfileHigh10        = 0x6e,
+    kAVCProfileHigh422       = 0x7a,
+    kAVCProfileHigh444       = 0xf4,
+    kAVCProfileCAVLC444Intra = 0x2c
+};
+
+void FindAVCDimensions(
+        const sp<ABuffer> &seqParamSet, int32_t *width, int32_t *height);
+
+unsigned parseUE(ABitReader *br);
+
+status_t getNextNALUnit(
+        const uint8_t **_data, size_t *_size,
+        const uint8_t **nalStart, size_t *nalSize,
+        bool startCodeFollows = false);
+
+struct MetaData;
+sp<MetaData> MakeAVCCodecSpecificData(const sp<ABuffer> &accessUnit);
+
+bool IsIDR(const sp<ABuffer> &accessUnit);
+
+const char *AVCProfileToString(uint8_t profile);
+
+sp<MetaData> MakeAACCodecSpecificData(
+        unsigned profile, unsigned sampling_freq_index,
+        unsigned channel_configuration);
+
+// Given an MPEG4 video VOL-header chunk (starting with 0x00 0x00 0x01 0x2?)
+// parse it and fill in dimensions, returns true iff successful.
+bool ExtractDimensionsFromVOLHeader(
+        const uint8_t *data, size_t size, int32_t *width, int32_t *height);
+
+bool GetMPEGAudioFrameSize(
+        uint32_t header, size_t *frame_size,
+        int *out_sampling_rate = NULL, int *out_channels = NULL,
+        int *out_bitrate = NULL, int *out_num_samples = NULL);
+
+}  // namespace android
+
+#endif  // AVC_UTILS_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/AAtomizer.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/AAtomizer.h
new file mode 100644
index 0000000..c1cafd7
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/AAtomizer.h
@@ -0,0 +1,53 @@
+/*
+ * 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 A_ATOMIZER_H_
+
+#define A_ATOMIZER_H_
+
+#include <stdint.h>
+
+#include "ABase.h"
+
+#include <utils/threads.h>
+
+#include <list>
+#include <string>
+#include <vector>
+
+namespace android {
+
+struct AAtomizer {
+    static const char *Atomize(const char *name);
+
+private:
+    static AAtomizer gAtomizer;
+
+    Mutex mLock;
+    std::vector<std::list<std::string>> mAtoms;
+
+    AAtomizer();
+
+    const char *atomize(const char *name);
+
+    static uint32_t Hash(const char *s);
+
+    DISALLOW_EVIL_CONSTRUCTORS(AAtomizer);
+};
+
+}  // namespace android
+
+#endif  // A_ATOMIZER_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ABase.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ABase.h
new file mode 100644
index 0000000..9eceea3
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ABase.h
@@ -0,0 +1,25 @@
+/*
+ * 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 A_BASE_H_
+
+#define A_BASE_H_
+
+#define DISALLOW_EVIL_CONSTRUCTORS(name) \
+    name(const name &); \
+    name &operator=(const name &)
+
+#endif  // A_BASE_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ABitReader.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ABitReader.h
new file mode 100644
index 0000000..5135211
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ABitReader.h
@@ -0,0 +1,53 @@
+/*
+ * 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 A_BIT_READER_H_
+
+#define A_BIT_READER_H_
+
+#include <media/stagefright/foundation/ABase.h>
+
+#include <sys/types.h>
+#include <stdint.h>
+
+namespace android {
+
+struct ABitReader {
+    ABitReader(const uint8_t *data, size_t size);
+
+    uint32_t getBits(size_t n);
+    void skipBits(size_t n);
+
+    size_t numBitsLeft() const;
+
+    const uint8_t *data() const;
+
+private:
+    const uint8_t *mData;
+    size_t mSize;
+
+    uint32_t mReservoir;  // left-aligned bits
+    size_t mNumBitsLeft;
+
+    void fillReservoir();
+    void putBits(uint32_t x, size_t n);
+
+    DISALLOW_EVIL_CONSTRUCTORS(ABitReader);
+};
+
+}  // namespace android
+
+#endif  // A_BIT_READER_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ABuffer.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ABuffer.h
new file mode 100644
index 0000000..7467e41
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ABuffer.h
@@ -0,0 +1,74 @@
+/*
+ * 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 A_BUFFER_H_
+
+#define A_BUFFER_H_
+
+#include "ABase.h"
+
+#include <sys/types.h>
+#include <stdint.h>
+
+#include <utils/RefBase.h>
+
+namespace android {
+
+struct AMessage;
+
+struct ABuffer : public RefBase {
+    ABuffer(size_t capacity);
+    ABuffer(void *data, size_t capacity);
+
+    void setFarewellMessage(const sp<AMessage> msg);
+
+    uint8_t *base() { return (uint8_t *)mData; }
+    uint8_t *data() { return (uint8_t *)mData + mRangeOffset; }
+    size_t capacity() const { return mCapacity; }
+    size_t size() const { return mRangeLength; }
+    size_t offset() const { return mRangeOffset; }
+
+    void setRange(size_t offset, size_t size);
+
+    void setInt32Data(int32_t data) { mInt32Data = data; }
+    int32_t int32Data() const { return mInt32Data; }
+
+    sp<AMessage> meta();
+
+    void reserve(size_t size);
+
+protected:
+    virtual ~ABuffer();
+
+private:
+    sp<AMessage> mFarewell;
+    sp<AMessage> mMeta;
+
+    void *mData;
+    size_t mCapacity;
+    size_t mRangeOffset;
+    size_t mRangeLength;
+
+    int32_t mInt32Data;
+
+    bool mOwnsData;
+
+    DISALLOW_EVIL_CONSTRUCTORS(ABuffer);
+};
+
+}  // namespace android
+
+#endif  // A_BUFFER_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ADebug.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ADebug.h
new file mode 100644
index 0000000..d415718
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ADebug.h
@@ -0,0 +1,150 @@
+#ifndef A_DEBUG_H_
+
+#define A_DEBUG_H_
+
+#ifdef TARGET_ANDROID
+#include <android-base/logging.h>
+#else
+
+#include "ABase.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <string>
+#include <string_view>
+
+enum LogType {
+    VERBOSE,
+    INFO,
+    WARNING,
+    ERROR,
+    FATAL,
+};
+
+namespace android {
+
+struct Logger {
+    Logger(LogType type);
+    virtual ~Logger();
+
+    Logger &operator<<(std::string_view x) {
+        mMessage.append(x);
+        return *this;
+    }
+
+    Logger &operator<<(const std::string &x) {
+        mMessage.append(x);
+        return *this;
+    }
+
+    Logger &operator<<(const char *x) {
+        mMessage.append(x);
+        return *this;
+    }
+
+    Logger &operator<<(char *x) {
+        mMessage.append(x);
+        return *this;
+    }
+
+    template<class T> Logger &operator<<(const T &x) {
+        mMessage.append(std::to_string(x));
+        return *this;
+    }
+
+private:
+    std::string mMessage;
+    LogType mLogType;
+
+    DISALLOW_EVIL_CONSTRUCTORS(Logger);
+};
+
+const char *LeafName(const char *s);
+
+#undef LOG
+#define LOG(type)                                                       \
+    android::Logger(type)                                               \
+        << android::LeafName(__FILE__) << ":" << __LINE__ << " "
+
+#define CHECK(condition)                                \
+    do {                                                \
+        if (!(condition)) {                             \
+            LOG(FATAL) << "CHECK(" #condition ") failed.";    \
+        }                                               \
+    } while (false)
+
+using std::to_string;
+
+inline std::string to_string(std::string_view s) {
+    return std::string(s);
+}
+
+#define MAKE_COMPARATOR(suffix,op)                          \
+    template<class A, class B>                              \
+    std::string Compare_##suffix(const A &a, const B &b) {  \
+        std::string res;                                    \
+        if (!(a op b)) {                                    \
+            res.append(to_string(a));                       \
+            res.append(" vs. ");                            \
+            res.append(to_string(b));                       \
+        }                                                   \
+        return res;                                         \
+    }
+
+MAKE_COMPARATOR(EQ,==)
+MAKE_COMPARATOR(NE,!=)
+MAKE_COMPARATOR(LE,<=)
+MAKE_COMPARATOR(GE,>=)
+MAKE_COMPARATOR(LT,<)
+MAKE_COMPARATOR(GT,>)
+
+#define CHECK_OP(x,y,suffix,op)                                         \
+    do {                                                                \
+        std::string ___res = android::Compare_##suffix(x, y);           \
+        if (!___res.empty()) {                                          \
+            LOG(FATAL) << "CHECK_" #suffix "(" #x "," #y ") failed: "   \
+                       << ___res;                                       \
+        }                                                               \
+    } while (false)
+
+#define CHECK_EQ(x,y)   CHECK_OP(x,y,EQ,==)
+#define CHECK_NE(x,y)   CHECK_OP(x,y,NE,!=)
+#define CHECK_LE(x,y)   CHECK_OP(x,y,LE,<=)
+#define CHECK_LT(x,y)   CHECK_OP(x,y,LT,<)
+#define CHECK_GE(x,y)   CHECK_OP(x,y,GE,>=)
+#define CHECK_GT(x,y)   CHECK_OP(x,y,GT,>)
+
+}  // namespace android
+
+#endif  // defined(TARGET_ANDROID)
+
+namespace android {
+
+#define TRESPASS()      LOG(FATAL) << "Should not be here."
+
+template<char prefix>
+void Log(const char *fmt, ...) {
+    va_list ap;
+    va_start(ap, fmt);
+    printf("%c ", prefix);
+    vprintf(fmt, ap);
+    printf("\n");
+    va_end(ap);
+}
+
+#ifdef LOG_NDEBUG
+#define ALOGV   Log<'V'>
+#else
+#define ALOGV(...)
+#endif
+
+#define ALOGE   Log<'E'>
+#define ALOGI   Log<'I'>
+#define ALOGW   Log<'W'>
+
+}  // namespace android
+
+#endif  // A_DEBUG_H_
+
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/AHandler.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/AHandler.h
new file mode 100644
index 0000000..d8bed5a
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/AHandler.h
@@ -0,0 +1,60 @@
+/*
+ * 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 A_HANDLER_H_
+
+#define A_HANDLER_H_
+
+#include "ALooper.h"
+
+#include <utils/RefBase.h>
+
+namespace android {
+
+struct AMessage;
+
+struct AHandler : public RefBase {
+    AHandler()
+        : mID(0) {
+    }
+
+    ALooper::handler_id id() const {
+        return mID;
+    }
+
+    ALooper *looper() const {
+        return mLooper;
+    }
+
+protected:
+    virtual void onMessageReceived(const sp<AMessage> &msg) = 0;
+
+private:
+    friend struct ALooperRoster;
+
+    ALooper::handler_id mID;
+    ALooper *mLooper;
+
+    void setID(ALooper::handler_id id) {
+        mID = id;
+    }
+
+    DISALLOW_EVIL_CONSTRUCTORS(AHandler);
+};
+
+}  // namespace android
+
+#endif  // A_HANDLER_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ALooper.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ALooper.h
new file mode 100644
index 0000000..a8ed67e
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ALooper.h
@@ -0,0 +1,78 @@
+/*
+ * 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 A_LOOPER_H_
+
+#define A_LOOPER_H_
+
+#include "ABase.h"
+
+#include <utils/Errors.h>
+#include <utils/RefBase.h>
+#include <utils/threads.h>
+
+#include <deque>
+
+namespace android {
+
+struct AHandler;
+struct AMessage;
+
+struct ALooper : public RefBase {
+    typedef int32_t event_id;
+    typedef int32_t handler_id;
+
+    ALooper();
+
+    handler_id registerHandler(const sp<AHandler> &handler);
+    void unregisterHandler(handler_id handlerID);
+
+    status_t start(bool runOnCallingThread = false);
+    status_t stop();
+
+    void setName(const char * /* name */) {}
+
+    static int64_t GetNowUs();
+
+protected:
+    virtual ~ALooper();
+
+private:
+    friend struct ALooperRoster;
+
+    struct Event {
+        int64_t mWhenUs;
+        sp<AMessage> mMessage;
+    };
+
+    Mutex mLock;
+    Condition mQueueChangedCondition;
+
+    std::deque<Event> mEventQueue;
+
+    struct LooperThread;
+    sp<LooperThread> mThread;
+    bool mRunningLocally;
+
+    void post(const sp<AMessage> &msg, int64_t delayUs);
+    bool loop();
+
+    DISALLOW_EVIL_CONSTRUCTORS(ALooper);
+};
+
+}  // namespace android
+
+#endif  // A_LOOPER_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ALooperRoster.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ALooperRoster.h
new file mode 100644
index 0000000..3e80088
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ALooperRoster.h
@@ -0,0 +1,53 @@
+/*
+ * 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 A_LOOPER_ROSTER_H_
+
+#define A_LOOPER_ROSTER_H_
+
+#include "ALooper.h"
+
+#include <map>
+
+namespace android {
+
+struct ALooperRoster {
+    ALooperRoster();
+
+    ALooper::handler_id registerHandler(
+            const sp<ALooper> looper, const sp<AHandler> &handler);
+
+    void unregisterHandler(ALooper::handler_id handlerID);
+
+    void postMessage(const sp<AMessage> &msg, int64_t delayUs = 0);
+    void deliverMessage(const sp<AMessage> &msg);
+
+private:
+    struct HandlerInfo {
+        sp<ALooper> mLooper;
+        wp<AHandler> mHandler;
+    };
+
+    Mutex mLock;
+    std::map<ALooper::handler_id, HandlerInfo> mHandlers;
+    ALooper::handler_id mNextHandlerID;
+
+    DISALLOW_EVIL_CONSTRUCTORS(ALooperRoster);
+};
+
+}  // namespace android
+
+#endif  // A_LOOPER_ROSTER_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/AMessage.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/AMessage.h
new file mode 100644
index 0000000..0b24f75
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/AMessage.h
@@ -0,0 +1,124 @@
+/*
+ * 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 A_MESSAGE_H_
+
+#define A_MESSAGE_H_
+
+#include <utils/RefBase.h>
+
+#include "ABase.h"
+#include "ALooper.h"
+
+#include <string>
+
+namespace android {
+
+struct ABuffer;
+
+struct AMessage : public RefBase {
+    explicit AMessage(uint32_t what = 0, ALooper::handler_id target = 0);
+
+    AMessage(const AMessage &) = delete;
+    AMessage &operator=(const AMessage &) = delete;
+
+    void setWhat(uint32_t what);
+    uint32_t what() const;
+
+    void setTarget(ALooper::handler_id target);
+    ALooper::handler_id target() const;
+
+    void setInt32(const char *name, int32_t value);
+    void setInt64(const char *name, int64_t value);
+    void setSize(const char *name, size_t value);
+    void setFloat(const char *name, float value);
+    void setDouble(const char *name, double value);
+    void setPointer(const char *name, void *value);
+    void setString(const char *name, const char *s, ssize_t len = -1);
+    void setObject(const char *name, const sp<RefBase> &obj);
+    void setMessage(const char *name, const sp<AMessage> &obj);
+    void setBuffer(const char *name, const sp<ABuffer> &obj);
+
+    bool findInt32(const char *name, int32_t *value) const;
+    bool findInt64(const char *name, int64_t *value) const;
+    bool findSize(const char *name, size_t *value) const;
+    bool findFloat(const char *name, float *value) const;
+    bool findDouble(const char *name, double *value) const;
+    bool findPointer(const char *name, void **value) const;
+    bool findString(const char *name, std::string *value) const;
+    bool findObject(const char *name, sp<RefBase> *obj) const;
+    bool findMessage(const char *name, sp<AMessage> *obj) const;
+    bool findBuffer(const char *name, sp<ABuffer> *obj) const;
+
+    void post(int64_t delayUs = 0);
+
+    sp<AMessage> dup() const;
+
+    std::string debugString(size_t indent = 0) const;
+
+    size_t countEntries() const;
+
+    enum Type {
+        kTypeInt32,
+        kTypeInt64,
+        kTypeSize,
+        kTypeFloat,
+        kTypeDouble,
+        kTypePointer,
+        kTypeString,
+        kTypeObject,
+        kTypeMessage,
+        kTypeBuffer,
+    };
+    const char *getEntryNameAt(size_t i, Type *type) const;
+
+protected:
+    virtual ~AMessage();
+
+private:
+    uint32_t mWhat;
+    ALooper::handler_id mTarget;
+
+    struct Item {
+        union {
+            int32_t int32Value;
+            int64_t int64Value;
+            size_t sizeValue;
+            float floatValue;
+            double doubleValue;
+            void *ptrValue;
+            RefBase *refValue;
+            std::string *stringValue;
+        } u;
+        const char *mName;
+        Type mType;
+    };
+
+    enum {
+        kMaxNumItems = 16
+    };
+    Item mItems[kMaxNumItems];
+    size_t mNumItems;
+
+    void clear();
+    Item *allocateItem(const char *name);
+    void freeItem(Item *item);
+    const Item *findItem(const char *name, Type type) const;
+};
+
+}  // namespace android
+
+#endif  // A_MESSAGE_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ANetworkSession.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ANetworkSession.h
new file mode 100644
index 0000000..3fa26ba
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ANetworkSession.h
@@ -0,0 +1,136 @@
+/*
+ * 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 A_NETWORK_SESSION_H_
+
+#define A_NETWORK_SESSION_H_
+
+#include <media/stagefright/foundation/ABase.h>
+#include <utils/RefBase.h>
+#include <utils/threads.h>
+
+#include <netinet/in.h>
+
+#include <map>
+
+namespace android {
+
+struct AMessage;
+
+// Helper class to manage a number of live sockets (datagram and stream-based)
+// on a single thread. Clients are notified about activity through AMessages.
+struct ANetworkSession : public RefBase {
+    ANetworkSession();
+
+    status_t start();
+    status_t stop();
+
+    status_t createRTSPClient(
+            const char *host, unsigned port, const sp<AMessage> &notify,
+            int32_t *sessionID);
+
+    status_t createRTSPServer(
+            const struct in_addr &addr, unsigned port,
+            const sp<AMessage> &notify, int32_t *sessionID);
+
+    status_t createUDPSession(
+            unsigned localPort, const sp<AMessage> &notify, int32_t *sessionID);
+
+    status_t createUDPSession(
+            unsigned localPort,
+            const char *remoteHost,
+            unsigned remotePort,
+            const sp<AMessage> &notify,
+            int32_t *sessionID);
+
+    status_t connectUDPSession(
+            int32_t sessionID, const char *remoteHost, unsigned remotePort);
+
+    // passive
+    status_t createTCPDatagramSession(
+            const struct in_addr &addr, unsigned port,
+            const sp<AMessage> &notify, int32_t *sessionID);
+
+    // active
+    status_t createTCPDatagramSession(
+            unsigned localPort,
+            const char *remoteHost,
+            unsigned remotePort,
+            const sp<AMessage> &notify,
+            int32_t *sessionID);
+
+    status_t destroySession(int32_t sessionID);
+
+    status_t sendRequest(
+            int32_t sessionID, const void *data, ssize_t size = -1,
+            bool timeValid = false, int64_t timeUs = -1ll);
+
+    status_t switchToWebSocketMode(int32_t sessionID);
+
+    enum NotificationReason {
+        kWhatError,
+        kWhatConnected,
+        kWhatClientConnected,
+        kWhatData,
+        kWhatDatagram,
+        kWhatBinaryData,
+        kWhatWebSocketMessage,
+        kWhatNetworkStall,
+    };
+
+protected:
+    virtual ~ANetworkSession();
+
+private:
+    struct NetworkThread;
+    struct Session;
+
+    Mutex mLock;
+    sp<Thread> mThread;
+
+    int32_t mNextSessionID;
+
+    int mPipeFd[2];
+
+    std::map<int32_t, sp<Session> > mSessions;
+
+    enum Mode {
+        kModeCreateUDPSession,
+        kModeCreateTCPDatagramSessionPassive,
+        kModeCreateTCPDatagramSessionActive,
+        kModeCreateRTSPServer,
+        kModeCreateRTSPClient,
+    };
+    status_t createClientOrServer(
+            Mode mode,
+            const struct in_addr *addr,
+            unsigned port,
+            const char *remoteHost,
+            unsigned remotePort,
+            const sp<AMessage> &notify,
+            int32_t *sessionID);
+
+    void threadLoop();
+    void interrupt();
+
+    static status_t MakeSocketNonBlocking(int s);
+
+    DISALLOW_EVIL_CONSTRUCTORS(ANetworkSession);
+};
+
+}  // namespace android
+
+#endif  // A_NETWORK_SESSION_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/JSONObject.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/JSONObject.h
new file mode 100644
index 0000000..7914282
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/JSONObject.h
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2013 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 JSON_OBJECT_H_
+
+#define JSON_OBJECT_H_
+
+#include <media/stagefright/foundation/ABase.h>
+#include <utils/Errors.h>
+#include <utils/RefBase.h>
+
+#include <map>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace android {
+
+struct JSONArray;
+struct JSONCompound;
+struct JSONObject;
+
+struct JSONValue {
+    enum FieldType {
+        TYPE_STRING,
+        TYPE_NUMBER,
+        TYPE_BOOLEAN,
+        TYPE_NULL,
+        TYPE_OBJECT,
+        TYPE_ARRAY,
+    };
+
+    // Returns the number of bytes consumed or an error.
+    static ssize_t Parse(const char *data, size_t size, JSONValue *out);
+
+    JSONValue();
+    JSONValue(const JSONValue &);
+    JSONValue &operator=(const JSONValue &);
+    ~JSONValue();
+
+    FieldType type() const;
+    bool getInt32(int32_t *value) const;
+    bool getString(std::string *value) const;
+    bool getBoolean(bool *value) const;
+    bool getObject(sp<JSONObject> *value) const;
+    bool getArray(sp<JSONArray> *value) const;
+
+    void setInt32(int32_t value);
+    void setString(std::string_view value);
+    void setBoolean(bool value);
+    void setObject(const sp<JSONObject> &obj);
+    void setArray(const sp<JSONArray> &array);
+    void unset();  // i.e. setNull()
+
+    std::string toString(size_t depth = 0, bool indentFirstLine = true) const;
+
+private:
+    FieldType mType;
+
+    union {
+        int32_t mInt32;
+        std::string *mString;
+        bool mBoolean;
+        JSONCompound *mObjectOrArray;
+    } mValue;
+};
+
+struct JSONCompound : public RefBase {
+    JSONCompound(const JSONCompound &) = delete;
+    JSONCompound &operator=(const JSONCompound &) = delete;
+
+    static sp<JSONCompound> Parse(const char *data, size_t size);
+
+    std::string toString(size_t depth = 0, bool indentFirstLine = true) const;
+
+    virtual bool isObject() const = 0;
+
+protected:
+    virtual ~JSONCompound() {}
+
+    virtual std::string internalToString(
+            size_t depth, bool indentFirstLine) const = 0;
+
+    JSONCompound() {}
+
+private:
+    friend struct JSONValue;
+};
+
+template<class KEY>
+struct JSONBase : public JSONCompound {
+    explicit JSONBase() {}
+
+    JSONBase(const JSONBase &) = delete;
+    JSONBase &operator=(const JSONBase &) = delete;
+
+#define PREAMBLE()                              \
+    JSONValue value;                            \
+    if (!getValue(key, &value)) {               \
+        return false;                           \
+    }
+
+    bool getFieldType(KEY key, JSONValue::FieldType *type) const {
+        PREAMBLE()
+        *type = value.type();
+        return true;
+    }
+
+    bool getInt32(KEY key, int32_t *out) const {
+        PREAMBLE()
+        return value.getInt32(out);
+    }
+
+    bool getString(KEY key, std::string *out) const {
+        PREAMBLE()
+        return value.getString(out);
+    }
+
+    bool getBoolean(KEY key, bool *out) const {
+        PREAMBLE()
+        return value.getBoolean(out);
+    }
+
+    bool getObject(KEY key, sp<JSONObject> *obj) const {
+        PREAMBLE()
+        return value.getObject(obj);
+    }
+
+    bool getArray(KEY key, sp<JSONArray> *obj) const {
+        PREAMBLE()
+        return value.getArray(obj);
+    }
+
+#undef PREAMBLE
+
+protected:
+    virtual ~JSONBase() {}
+
+    virtual bool getValue(KEY key, JSONValue *value) const = 0;
+};
+
+struct JSONObject : public JSONBase<const char *> {
+    explicit JSONObject();
+
+    JSONObject(const JSONObject &) = delete;
+    JSONObject &operator=(const JSONObject &) = delete;
+
+    virtual bool isObject() const;
+    void setValue(const char *key, const JSONValue &value);
+
+    void setInt32(const char *key, int32_t in) {
+        JSONValue val;
+        val.setInt32(in);
+        setValue(key, val);
+    }
+
+    void setString(const char *key, std::string_view in) {
+        JSONValue val;
+        val.setString(in);
+        setValue(key, val);
+    }
+
+    void setBoolean(const char *key, bool in) {
+        JSONValue val;
+        val.setBoolean(in);
+        setValue(key, val);
+    }
+
+    void setObject(const char *key, const sp<JSONObject> &obj) {
+        JSONValue val;
+        val.setObject(obj);
+        setValue(key, val);
+    }
+
+    void setArray(const char *key, const sp<JSONArray> &obj) {
+        JSONValue val;
+        val.setArray(obj);
+        setValue(key, val);
+    }
+
+    void remove(const char *key);
+
+protected:
+    virtual ~JSONObject();
+
+    virtual bool getValue(const char *key, JSONValue *value) const;
+    virtual std::string internalToString(size_t depth, bool indentFirstLine) const;
+
+private:
+    std::map<std::string, JSONValue> mValues;
+};
+
+struct JSONArray : public JSONBase<size_t> {
+    explicit JSONArray();
+
+    JSONArray(const JSONArray &) = delete;
+    JSONArray &operator=(const JSONArray &) = delete;
+
+    virtual bool isObject() const;
+    size_t size() const;
+    void addValue(const JSONValue &value);
+
+    void addInt32(int32_t in) {
+        JSONValue val;
+        val.setInt32(in);
+        addValue(val);
+    }
+
+    void addString(std::string_view in) {
+        JSONValue val;
+        val.setString(in);
+        addValue(val);
+    }
+
+    void addBoolean(bool in) {
+        JSONValue val;
+        val.setBoolean(in);
+        addValue(val);
+    }
+
+    void addObject(const sp<JSONObject> &obj) {
+        JSONValue val;
+        val.setObject(obj);
+        addValue(val);
+    }
+
+    void addArray(const sp<JSONArray> &obj) {
+        JSONValue val;
+        val.setArray(obj);
+        addValue(val);
+    }
+
+protected:
+    virtual ~JSONArray();
+
+    virtual bool getValue(size_t key, JSONValue *value) const;
+    virtual std::string internalToString(size_t depth, bool indentFirstLine) const;
+
+
+private:
+    std::vector<JSONValue> mValues;
+};
+
+}  // namespace android
+
+#endif  // JSON_OBJECT_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ParsedMessage.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ParsedMessage.h
new file mode 100644
index 0000000..504e17b
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ParsedMessage.h
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+#include <media/stagefright/foundation/ABase.h>
+#include <utils/RefBase.h>
+
+#include <map>
+#include <string>
+
+namespace android {
+
+// Encapsulates an "HTTP/RTSP style" response, i.e. a status line,
+// key/value pairs making up the headers and an optional body/content.
+struct ParsedMessage : public RefBase {
+    static sp<ParsedMessage> Parse(
+            const char *data, size_t size, bool noMoreData, size_t *length);
+
+    bool findString(const char *name, std::string *value) const;
+    bool findInt32(const char *name, int32_t *value) const;
+
+    const char *getContent() const;
+
+    bool getRequestField(size_t index, std::string *field) const;
+    bool getStatusCode(int32_t *statusCode) const;
+
+    std::string debugString() const;
+
+    static bool GetAttribute(
+            const char *s, const char *key, std::string *value);
+
+    static bool GetInt32Attribute(
+            const char *s, const char *key, int32_t *value);
+
+
+protected:
+    virtual ~ParsedMessage();
+
+private:
+    std::map<std::string, std::string> mDict;
+    std::string mContent;
+
+    ParsedMessage();
+
+    ssize_t parse(const char *data, size_t size, bool noMoreData);
+
+    DISALLOW_EVIL_CONSTRUCTORS(ParsedMessage);
+};
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ReflectorHandler.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ReflectorHandler.h
new file mode 100644
index 0000000..cc1cadc
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/ReflectorHandler.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <media/stagefright/foundation/AHandler.h>
+
+namespace android {
+
+template<class T>
+struct ReflectorHandler : public AHandler {
+    explicit ReflectorHandler(T *target)
+        : mTarget(target) {
+    }
+
+    void onMessageReceived(const sp<AMessage> &msg) {
+        mTarget->onMessageReceived(msg);
+    }
+
+private:
+    T *mTarget;
+};
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/TSPacketizer.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/TSPacketizer.h
new file mode 100644
index 0000000..ecfc41d
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/TSPacketizer.h
@@ -0,0 +1,74 @@
+/*
+ * 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 TS_PACKETIZER_H_
+
+#define TS_PACKETIZER_H_
+
+#include <media/stagefright/foundation/ABase.h>
+#include <utils/Errors.h>
+#include <utils/RefBase.h>
+
+#include <vector>
+
+namespace android {
+
+struct ABuffer;
+struct AMessage;
+
+struct TSPacketizer : public RefBase {
+    TSPacketizer();
+
+    // Returns trackIndex or error.
+    ssize_t addTrack(const sp<AMessage> &format);
+
+    enum {
+        EMIT_PAT_AND_PMT = 1,
+        EMIT_PCR         = 2,
+    };
+    status_t packetize(
+            size_t trackIndex, const sp<ABuffer> &accessUnit,
+            sp<ABuffer> *packets,
+            uint32_t flags);
+
+protected:
+    virtual ~TSPacketizer();
+
+private:
+    enum {
+        kPID_PMT = 0x100,
+        kPID_PCR = 0x1000,
+    };
+
+    struct Track;
+
+    std::vector<sp<Track>> mTracks;
+
+    unsigned mPATContinuityCounter;
+    unsigned mPMTContinuityCounter;
+
+    uint32_t mCrcTable[256];
+
+    void initCrcTable();
+    uint32_t crc32(const uint8_t *start, size_t size) const;
+
+    DISALLOW_EVIL_CONSTRUCTORS(TSPacketizer);
+};
+
+}  // namespace android
+
+#endif  // TS_PACKETIZER_H_
+
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/base64.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/base64.h
new file mode 100644
index 0000000..e4f627b
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/base64.h
@@ -0,0 +1,19 @@
+#ifndef BASE_64_H_
+
+#define BASE_64_H_
+
+#include <utils/RefBase.h>
+
+#include <string>
+#include <string_view>
+
+namespace android {
+
+struct ABuffer;
+
+sp<ABuffer> decodeBase64(const std::string_view &s);
+void encodeBase64(const void *data, size_t size, std::string *out);
+
+}  // namespace android
+
+#endif  // BASE_64_H_
diff --git a/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/hexdump.h b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/hexdump.h
new file mode 100644
index 0000000..99b8fc1
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/media/stagefright/foundation/hexdump.h
@@ -0,0 +1,19 @@
+#ifndef HEXDUMP_H_
+
+#define HEXDUMP_H_
+
+#include <sys/types.h>
+
+#include <string>
+
+namespace android {
+
+void hexdump(
+        const void *_data,
+        size_t size,
+        size_t indent = 0,
+        std::string *appendTo = nullptr);
+
+}  // namespace android
+
+#endif  // HEXDUMP_H_
diff --git a/host/frontend/gcastv2/libandroid/include/utils/Errors.h b/host/frontend/gcastv2/libandroid/include/utils/Errors.h
new file mode 100644
index 0000000..6827e17
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/utils/Errors.h
@@ -0,0 +1,23 @@
+#ifndef ANDROID_ERRORS_H_
+
+#define ANDROID_ERRORS_H_
+
+#include <errno.h>
+#include <stdint.h>
+
+namespace android {
+
+typedef int32_t status_t;
+
+enum {
+    OK                = 0,
+    UNKNOWN_ERROR     = -1,
+    INVALID_OPERATION = -EINVAL,
+    NO_INIT           = -ENODEV,
+    ERROR_IO          = -EIO,
+    ERROR_UNSUPPORTED = INVALID_OPERATION,
+};
+
+}  // namespace android
+
+#endif  // ANDROID_ERRORS_H_
diff --git a/host/frontend/gcastv2/libandroid/include/utils/KeyStore.h b/host/frontend/gcastv2/libandroid/include/utils/KeyStore.h
new file mode 100644
index 0000000..1cbfd55
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/utils/KeyStore.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+void setCertificateOrKey(
+        const std::string &name, const void *_data, size_t size);
+
+bool getCertificateOrKey(
+        const std::string &name, std::vector<uint8_t> *data);
+
diff --git a/host/frontend/gcastv2/libandroid/include/utils/Log.h b/host/frontend/gcastv2/libandroid/include/utils/Log.h
new file mode 100644
index 0000000..5c73c18
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/utils/Log.h
@@ -0,0 +1,7 @@
+#ifndef ANDROID_LOG_H_
+
+#define ANDROID_LOG_H_
+
+#include <media/stagefright/foundation/ADebug.h>
+
+#endif  // ANDROID_LOG_H_
diff --git a/host/frontend/gcastv2/libandroid/include/utils/RefBase.h b/host/frontend/gcastv2/libandroid/include/utils/RefBase.h
new file mode 100644
index 0000000..de37c51
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/utils/RefBase.h
@@ -0,0 +1,234 @@
+#ifndef ANDROID_REFBASE_H_
+
+#define ANDROID_REFBASE_H_
+
+#include <stdlib.h>
+
+#include <media/stagefright/foundation/ABase.h>
+#include <media/stagefright/foundation/ADebug.h>
+
+#include <atomic>
+#include <list>
+
+namespace android {
+
+struct RefBase;
+
+// XXX Warning: The weak ref handling is NOT threadsafe yet.
+struct WeakList {
+    WeakList(RefBase *obj)
+        : mObject(obj) {
+    }
+
+    void add(void *id) {
+        mRefs.push_back(id);
+    }
+
+    void remove(void *id) {
+        auto it = mRefs.begin();
+        while (it != mRefs.end() && *it != id) {
+            ++it;
+        }
+        CHECK(it != mRefs.end());
+        mRefs.erase(it);
+
+        if (mRefs.empty() && mObject == NULL) {
+            delete this;
+        }
+    }
+
+    RefBase *promote() const;
+
+private:
+    friend struct RefBase;
+
+    RefBase *mObject;
+    std::list<void *> mRefs;
+
+    ~WeakList() {
+        CHECK(mRefs.empty());
+    }
+
+    void objectDied() {
+        mObject = NULL;
+        if (mRefs.empty()) {
+            delete this;
+        }
+    }
+
+    DISALLOW_EVIL_CONSTRUCTORS(WeakList);
+};
+
+struct RefBase {
+    RefBase()
+        : mNumRefs(0),
+          mWeakList(NULL) {
+    }
+
+    void incStrong(const void *) {
+        ++mNumRefs;
+    }
+
+    void decStrong(const void *) {
+        if (--mNumRefs == 0) {
+            if (mWeakList != NULL) {
+                mWeakList->objectDied();
+                mWeakList = NULL;
+            }
+
+            delete this;
+        }
+    }
+
+    WeakList *getWeakRefs() {
+        if (mWeakList == NULL) {
+            mWeakList = new WeakList(this);
+        }
+
+        return mWeakList;
+    }
+
+protected:
+    virtual ~RefBase() {}
+
+private:
+    std::atomic<int32_t> mNumRefs;
+    WeakList *mWeakList;
+
+    DISALLOW_EVIL_CONSTRUCTORS(RefBase);
+};
+
+template<class T>
+struct sp {
+    sp() : mObj(NULL) {}
+
+    sp(T *obj)
+        : mObj(obj) {
+        if (mObj) { mObj->incStrong(this); }
+    }
+
+    sp(const sp<T> &other)
+        : mObj(other.mObj) {
+        if (mObj) { mObj->incStrong(this); }
+    }
+
+    template<class U>
+    sp(const sp<U> &other)
+        : mObj(other.mObj) {
+        if (mObj) { mObj->incStrong(this); }
+    }
+
+    ~sp() {
+        if (mObj) { mObj->decStrong(this); }
+    }
+
+    T &operator*() const { return *mObj; }
+    T *operator->() const { return mObj; }
+    T *get() const { return mObj; }
+
+    sp<T> &operator=(T *obj) {
+        if (obj) { obj->incStrong(this); }
+        if (mObj) { mObj->decStrong(this); }
+        mObj = obj;
+
+        return *this;
+    }
+
+    sp<T> &operator=(const sp<T> &other) {
+        return (*this) = other.mObj;
+    }
+
+    template<class U>
+    sp<T> &operator=(const sp<U> &other) {
+        return (*this) = other.mObj;
+    }
+
+    void clear() {
+        if (mObj) { mObj->decStrong(this); mObj = NULL; }
+    }
+
+    bool operator==(const sp<T> &other) const {
+        return mObj == other.mObj;
+    }
+
+    bool operator!=(const sp<T> &other) const {
+        return mObj != other.mObj;
+    }
+
+    explicit operator bool() const {
+        return mObj != nullptr;
+    }
+
+private:
+    template<typename Y> friend struct sp;
+
+    T *mObj;
+};
+
+template<class T>
+struct wp {
+    wp() : mWeakList(NULL) {
+    }
+
+    wp(T *obj) {
+        if (obj != NULL) {
+          mWeakList = obj->getWeakRefs();
+          mWeakList->add(this);
+        } else {
+          mWeakList = NULL;
+        }
+    }
+
+    wp(const wp<T> &other)
+        : mWeakList(other.mWeakList) {
+        if (mWeakList != NULL) {
+            mWeakList->add(this);
+        }
+    }
+
+    wp<T> &operator=(const wp<T> &other) {
+        if (mWeakList != other.mWeakList) {
+            if (other.mWeakList != NULL) {
+                other.mWeakList->add(this);
+            }
+            if (mWeakList != NULL) {
+                mWeakList->remove(this);
+            }
+            mWeakList = other.mWeakList;
+        }
+
+        return *this;
+    }
+
+    ~wp() {
+        clear();
+    }
+
+    void clear() {
+        if (mWeakList != NULL) {
+            mWeakList->remove(this);
+            mWeakList = NULL;
+        }
+    }
+
+    sp<T> promote() const {
+        if (mWeakList == NULL) {
+            return NULL;
+        }
+
+        sp<T> result = (T *)mWeakList->promote();
+
+        if (result != NULL) {
+            result->decStrong(this);
+        }
+
+        return result;
+    }
+
+private:
+    WeakList *mWeakList;
+};
+
+}  // namespace android
+
+#endif  // ANDROID_REFBASE_H_
diff --git a/host/frontend/gcastv2/libandroid/include/utils/threads.h b/host/frontend/gcastv2/libandroid/include/utils/threads.h
new file mode 100644
index 0000000..f6e0cb1
--- /dev/null
+++ b/host/frontend/gcastv2/libandroid/include/utils/threads.h
@@ -0,0 +1,200 @@
+#ifndef ANDROID_THREADS_H_
+
+#define ANDROID_THREADS_H_
+
+#include <media/stagefright/foundation/ABase.h>
+#include <media/stagefright/foundation/ADebug.h>
+
+#include <sys/time.h>
+
+#include <utils/Errors.h>
+#include <utils/RefBase.h>
+
+#ifdef TARGET_ANDROID_DEVICE
+#include <helpers/JavaThread.h>
+#include <memory>
+#include <thread>
+#else
+#include <pthread.h>
+#endif
+
+namespace android {
+
+struct Thread : public RefBase {
+    Thread()
+#ifndef TARGET_ANDROID_DEVICE
+        : mThread(0)
+#endif
+    {
+    }
+
+    status_t run(const char *name) {
+        if (mThread != 0) {
+            return INVALID_OPERATION;
+        }
+
+        mName = name;
+
+        mExitRequested = false;
+#ifdef TARGET_ANDROID_DEVICE
+        mThread = std::make_unique<std::thread>(
+                createJavaThread([this] {
+                    (void)ThreadWrapper(this);
+                }));
+#else
+        int res = pthread_create(&mThread, NULL, &Thread::ThreadWrapper, this);
+
+        if (res != 0) {
+            mThread = 0;
+
+            return -errno;
+        }
+#endif
+
+        return OK;
+    }
+
+    void requestExit() { mExitRequested = true; }
+
+    void requestExitAndWait() {
+        requestExit();
+
+#ifdef TARGET_ANDROID_DEVICE
+        if (mThread) {
+            mThread->join();
+            mThread.reset();
+        }
+#else
+        void *dummy;
+        pthread_join(mThread, &dummy);
+        mThread = 0;
+#endif
+    }
+
+protected:
+    virtual bool threadLoop() = 0;
+
+    ~Thread() {
+        if (mThread) {
+            requestExitAndWait();
+
+            CHECK(!mThread);
+        }
+    }
+
+private:
+#ifdef TARGET_ANDROID_DEVICE
+    std::unique_ptr<std::thread> mThread;
+#else
+    pthread_t mThread;
+#endif
+    volatile bool mExitRequested;
+    std::string mName;
+
+    static void *ThreadWrapper(void *param) {
+        Thread *me = static_cast<Thread *>(param);
+
+        while (!me->mExitRequested) {
+            if (!me->threadLoop()) {
+                break;
+            }
+        }
+
+        return NULL;
+    }
+
+    DISALLOW_EVIL_CONSTRUCTORS(Thread);
+};
+
+struct Mutex {
+    Mutex() {
+        CHECK_EQ(pthread_mutex_init(&mMutex, NULL), 0);
+    }
+
+    ~Mutex() {
+        CHECK_EQ(pthread_mutex_destroy(&mMutex), 0);
+    }
+
+    void lock() {
+        CHECK_EQ(pthread_mutex_lock(&mMutex), 0);
+    }
+
+    void unlock() {
+        CHECK_EQ(pthread_mutex_unlock(&mMutex), 0);
+    }
+
+    struct Autolock;
+
+private:
+    friend struct Condition;
+
+    pthread_mutex_t mMutex;
+
+    DISALLOW_EVIL_CONSTRUCTORS(Mutex);
+};
+
+struct Mutex::Autolock {
+    Autolock(Mutex &mutex)
+        : mMutex(mutex) {
+        mMutex.lock();
+    }
+
+    ~Autolock() {
+        mMutex.unlock();
+    }
+
+private:
+    Mutex &mMutex;
+
+    DISALLOW_EVIL_CONSTRUCTORS(Autolock);
+};
+
+struct Condition {
+    Condition() {
+        CHECK_EQ(pthread_cond_init(&mCond, NULL), 0);
+    }
+
+    ~Condition() {
+        CHECK_EQ(pthread_cond_destroy(&mCond), 0);
+    }
+
+    void signal() {
+        CHECK_EQ(pthread_cond_signal(&mCond), 0);
+    }
+
+    void broadcast() {
+        CHECK_EQ(pthread_cond_broadcast(&mCond), 0);
+    }
+
+    status_t wait(Mutex &mutex) {
+        int res = pthread_cond_wait(&mCond, &mutex.mMutex);
+
+        return res == 0 ? OK : -errno;
+    }
+
+    status_t waitRelative(Mutex &mutex, int64_t nsecs) {
+        struct timeval tv;
+        gettimeofday(&tv, NULL);
+
+        struct timespec abstime;
+        abstime.tv_sec = tv.tv_sec;
+        abstime.tv_nsec = tv.tv_usec * 1000ll + nsecs;
+
+        int res = pthread_cond_timedwait(&mCond, &mutex.mMutex, &abstime);
+
+        if (res == 0) {
+            return OK;
+        }
+
+        return -errno;
+    }
+
+private:
+    pthread_cond_t mCond;
+
+    DISALLOW_EVIL_CONSTRUCTORS(Condition);
+};
+
+}  // namespace android
+
+#endif  // ANDROID_THREADS_H_
diff --git a/host/frontend/gcastv2/libsource/AACPlayer.cpp b/host/frontend/gcastv2/libsource/AACPlayer.cpp
new file mode 100644
index 0000000..eb1d761
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/AACPlayer.cpp
@@ -0,0 +1,496 @@
+//#define LOG_NDEBUG 0
+#define LOG_TAG "AACPlayer"
+#include <utils/Log.h>
+
+#include <source/AACPlayer.h>
+
+#include <algorithm>
+
+namespace android {
+
+static void check(OSStatus err, const char *file, int line) {
+    if (err == noErr) {
+        return;
+    }
+
+    ALOGE("check FAILED w/ error %d at %s:%d", err, file, line);
+    TRESPASS();
+}
+
+#define CHECK_OSSTATUS(err) do { check(err, __FILE__, __LINE__); } while (0)
+
+static constexpr int32_t kSampleRate[] = {
+    96000, 88200, 64000, 48000, 44100, 32000,
+    24000, 22050, 16000, 12000, 11025, 8000
+};
+
+AACPlayer::AACPlayer()
+    : mConverter(nullptr),
+#if USE_AUDIO_UNIT
+      mGraph(nullptr),
+#else
+      mQueue(nullptr),
+#endif
+      mSampleRateHz(-1),
+      mNumFramesSubmitted(0) {
+}
+
+AACPlayer::~AACPlayer() {
+#if USE_AUDIO_UNIT
+    if (mGraph) {
+        DisposeAUGraph(mGraph);
+        mGraph = nullptr;
+    }
+#else
+    if (mQueue) {
+        AudioQueueStop(mQueue, true /* immediate */);
+        AudioQueueDispose(mQueue, true /* immediate */);
+        mQueue = nullptr;
+    }
+#endif
+
+    if (mConverter) {
+        AudioConverterDispose(mConverter);
+        mConverter = nullptr;
+    }
+}
+
+struct FeedCookie {
+    const void *data;
+    size_t size;
+};
+
+static OSStatus FeedInputData(
+        AudioConverterRef /* converter */,
+        UInt32 *numDataPackets,
+        AudioBufferList *data,
+        AudioStreamPacketDescription **dataPacketDescription,
+        void *_cookie) {
+    FeedCookie *cookie = static_cast<FeedCookie *>(_cookie);
+
+    assert(*numDataPackets == 1);
+    assert(data->mNumberBuffers == 1);
+
+    assert(cookie->size > 0);
+    data->mBuffers[0].mNumberChannels = 0;
+    data->mBuffers[0].mDataByteSize = static_cast<UInt32>(cookie->size);
+    data->mBuffers[0].mData = const_cast<void *>(cookie->data);
+
+    if (dataPacketDescription) {
+        static AudioStreamPacketDescription desc;
+        desc.mDataByteSize = static_cast<UInt32>(cookie->size);
+        desc.mStartOffset = 0;
+        desc.mVariableFramesInPacket = 0;
+
+        *dataPacketDescription = &desc;
+    }
+
+    cookie->size = 0;
+
+    return noErr;
+}
+
+status_t AACPlayer::feedADTSFrame(const void *_frame, size_t size) {
+    const uint8_t *frame = static_cast<const uint8_t *>(_frame);
+
+    static constexpr size_t kADTSHeaderSize = 7;
+
+    if (size < kADTSHeaderSize) {
+        return -EINVAL;
+    }
+
+    if (frame[0] != 0xff || (frame[1] >> 4) != 0xf) {
+        return -EINVAL;
+    }
+
+    const size_t frameSize =
+        (static_cast<size_t>(frame[3]) & 0x3f) << 11
+        | static_cast<size_t>(frame[4]) << 3
+        | frame[5] >> 5;
+
+    if (size != frameSize) {
+        return -EINVAL;
+    }
+
+    if (mConverter == nullptr) {
+        // size_t profile = (frame[2] >> 6) + 1;
+        int32_t sampleRateIndex = (frame[2] >> 2) & 15;
+        int32_t channelCount = ((frame[2] & 3) << 2) | (frame[3] >> 6);
+
+        int32_t sampleRate = kSampleRate[sampleRateIndex];
+
+        status_t err = init(sampleRateIndex, channelCount);
+        if (err != OK) {
+            return err;
+        }
+
+        mSampleRateHz = sampleRate;
+    }
+
+#if USE_AUDIO_UNIT
+    struct OutputBuffer {
+        void *mAudioData;
+        UInt32 mAudioDataByteSize;
+    };
+
+    const size_t outBufferSize = mBufferQueue->bufferSize();
+    std::unique_ptr<OutputBuffer> outBuffer(new OutputBuffer);
+    outBuffer->mAudioData = mBufferQueue->acquire();
+    outBuffer->mAudioDataByteSize = 0;
+#else
+    const size_t outBufferSize = mBufferManager->bufferSize();
+    AudioQueueBufferRef outBuffer = mBufferManager->acquire();
+#endif
+
+    UInt32 outputDataPacketSize = mInFormat.mFramesPerPacket;
+    AudioBufferList outputData;
+    outputData.mNumberBuffers = 1;
+    outputData.mBuffers[0].mData = outBuffer->mAudioData;
+    outputData.mBuffers[0].mDataByteSize = static_cast<UInt32>(outBufferSize);
+    outputData.mBuffers[0].mNumberChannels = mInFormat.mChannelsPerFrame;
+
+    FeedCookie cookie;
+    cookie.data = &frame[kADTSHeaderSize];
+    cookie.size = frameSize - kADTSHeaderSize;
+
+    OSStatus err = AudioConverterFillComplexBuffer(
+            mConverter,
+            FeedInputData,
+            &cookie,
+            &outputDataPacketSize,
+            &outputData,
+            nullptr);
+
+    CHECK_OSSTATUS(err);
+
+    assert(outputDataPacketSize == mInFormat.mFramesPerPacket);
+    assert(outputData.mNumberBuffers == 1);
+
+    outBuffer->mAudioDataByteSize = outputData.mBuffers[0].mDataByteSize;
+
+#if USE_AUDIO_UNIT
+    mBufferQueue->queue(outBuffer->mAudioData);
+#else
+    err = AudioQueueEnqueueBuffer(
+            mQueue,
+            outBuffer,
+            0 /* numPacketDescs */,
+            nullptr /* packetDescs */);
+
+    CHECK_OSSTATUS(err);
+#endif
+
+    mNumFramesSubmitted += 1024;
+
+    return OK;
+}
+
+int32_t AACPlayer::sampleRateHz() const {
+    return mSampleRateHz;
+}
+
+static void writeInt16(uint8_t *&ptr, uint16_t x) {
+    *ptr++ = x >> 8;
+    *ptr++ = x & 0xff;
+}
+
+static void writeInt32(uint8_t *&ptr, uint32_t x) {
+    writeInt16(ptr, x >> 16);
+    writeInt16(ptr, x & 0xffff);
+}
+
+static void writeInt24(uint8_t *&ptr, uint32_t x) {
+    *ptr++ = (x >> 16) & 0xff;
+    writeInt16(ptr, x & 0xffff);
+}
+
+static void writeDescriptor(uint8_t *&ptr, uint8_t tag, size_t size) {
+    *ptr++ = tag;
+    for (size_t i = 3; i > 0; --i) {
+        *ptr++ = (size >> (7 * i)) | 0x80;
+    }
+    *ptr++ = size & 0x7f;
+}
+
+#if !USE_AUDIO_UNIT
+static void PropertyListenerCallback(
+        void * /* cookie */,
+        AudioQueueRef queue,
+        AudioQueuePropertyID /* propertyID */) {
+    UInt32 isRunning;
+    UInt32 size = sizeof(isRunning);
+
+    OSStatus err = AudioQueueGetProperty(
+            queue, kAudioQueueProperty_IsRunning, &isRunning, &size);
+
+    CHECK_OSSTATUS(err);
+    CHECK_EQ(size, sizeof(isRunning));
+
+    ALOGI("AudioQueue is now %s", isRunning ? "running" : "stopped");
+}
+#else
+// static
+OSStatus AACPlayer::FeedInput(
+        void *cookie,
+        AudioUnitRenderActionFlags * /* flags */,
+        const AudioTimeStamp * /* timeStamp */,
+        UInt32 /* bus */,
+        UInt32 numFrames,
+        AudioBufferList *data) {
+    AACPlayer *me = static_cast<AACPlayer *>(cookie);
+
+    UInt32 curFrame = 0;
+    void *outPtr = data->mBuffers[0].mData;
+
+    const size_t bytesPerFrame = me->mOutFormat.mBytesPerFrame;
+
+    while (curFrame < numFrames) {
+        size_t inSize;
+        const void *inData = me->mBufferQueue->dequeueBegin(&inSize);
+
+        if (inData == nullptr) {
+            // underrun
+            memset(outPtr, 0, (numFrames - curFrame) * bytesPerFrame);
+            break;
+        }
+
+        const size_t inSizeFrames = inSize / bytesPerFrame;
+
+        const size_t copyFrames =
+            std::min(inSizeFrames, static_cast<size_t>(numFrames - curFrame));
+
+        const size_t copyBytes = copyFrames * bytesPerFrame;
+
+        memcpy(outPtr, inData, copyBytes);
+        outPtr = (uint8_t *)outPtr + copyBytes;
+
+        me->mBufferQueue->dequeueEnd(inSize - copyBytes);
+
+        curFrame += copyFrames;
+    }
+
+    data->mBuffers[0].mDataByteSize = numFrames * static_cast<UInt32>(bytesPerFrame);
+
+    return noErr;
+}
+#endif
+
+status_t AACPlayer::init(int32_t sampleRateIndex, size_t channelCount) {
+    const int32_t sampleRate = kSampleRate[sampleRateIndex];
+
+    memset(&mOutFormat, 0, sizeof(mOutFormat));
+
+    mOutFormat.mSampleRate = static_cast<double>(sampleRate);
+    mOutFormat.mBitsPerChannel = 8 * sizeof(float);
+    mOutFormat.mChannelsPerFrame = static_cast<UInt32>(channelCount);
+
+    mOutFormat.mFormatID = kAudioFormatLinearPCM;
+    mOutFormat.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked;
+
+    mOutFormat.mFramesPerPacket = 1;
+
+    mOutFormat.mBytesPerFrame =
+        (mOutFormat.mBitsPerChannel / 8) * mOutFormat.mChannelsPerFrame;
+
+    mOutFormat.mBytesPerPacket =
+        mOutFormat.mBytesPerFrame * mOutFormat.mFramesPerPacket;
+
+    memset(&mInFormat, 0, sizeof(mInFormat));
+
+    mInFormat.mSampleRate = static_cast<double>(sampleRate);
+    mInFormat.mBitsPerChannel = 0;  // compressed
+    mInFormat.mChannelsPerFrame = static_cast<UInt32>(channelCount);
+
+    mInFormat.mFormatID = kAudioFormatMPEG4AAC;
+    mInFormat.mFormatFlags = kMPEG4Object_AAC_LC;
+
+    mInFormat.mFramesPerPacket = 1024;
+
+    mInFormat.mBytesPerFrame = 0;  // variable
+    mInFormat.mBytesPerPacket = 0;  // variable
+
+    OSStatus err = AudioConverterNew(&mInFormat, &mOutFormat, &mConverter);
+    CHECK_OSSTATUS(err);
+    assert(mConverter != nullptr);
+
+    // static constexpr uint8_t kAACCodecSpecificData[] = {0x11,0x90};
+    // static constexpr uint8_t kAACCodecSpecificData[] = {0x12,0x10};
+
+    // 5 bits: object type
+    // 4 bits: frequency index
+    // if (frequency index == 15) { 24 bits frequency }
+    // 4 bits: channel config
+    // 1 bit: frame length flag
+    // 1 bit: dependsOnCoreCoder
+    // 1 bit: extensionFlag
+
+    // 0x11 0x90 => 00010 0011 0010 0 0 0 (AAC LC, 48kHz, 2 channels)
+    // 0x12 0x10 => 00010 0100 0010 0 0 0 (AAC LC, 44.1kHz, 2 channels)
+
+    uint8_t kAACCodecSpecificData[2];
+
+    kAACCodecSpecificData[0] =
+        (2 << 3) /* AAC LC */ | (sampleRateIndex >> 1);
+
+    kAACCodecSpecificData[1] =
+        ((sampleRateIndex & 1) << 7) | (channelCount << 3);
+
+    static constexpr size_t kAACCodecSpecificDataSize =
+        sizeof(kAACCodecSpecificData);
+
+    uint8_t magic[128];
+    uint8_t *ptr = magic;
+    writeDescriptor(ptr, 0x03, 3 + 5 + 13 + 5 + kAACCodecSpecificDataSize);
+    writeInt16(ptr, 0x00);
+    *ptr++ = 0x00;
+
+    // DecoderConfig descriptor
+    writeDescriptor(ptr, 0x04, 13 + 5 + kAACCodecSpecificDataSize);
+
+    // Object type indication
+    *ptr++ = 0x40;
+
+    // Flags (= Audiostream)
+    *ptr++ = 0x15;
+
+    writeInt24(ptr, 0);  // BufferSize DB
+    writeInt32(ptr, 0);  // max bitrate
+    writeInt32(ptr, 0);  // avg bitrate
+
+    writeDescriptor(ptr, 0x05, kAACCodecSpecificDataSize);
+    memcpy(ptr, kAACCodecSpecificData, kAACCodecSpecificDataSize);
+    ptr += kAACCodecSpecificDataSize;
+
+    size_t magicSize = ptr - magic;
+
+    err = AudioConverterSetProperty(
+            mConverter,
+            kAudioConverterDecompressionMagicCookie,
+            static_cast<UInt32>(magicSize),
+            magic);
+
+    CHECK_OSSTATUS(err);
+
+#if USE_AUDIO_UNIT
+    err = NewAUGraph(&mGraph);
+    CHECK_OSSTATUS(err);
+
+    AudioComponentDescription desc;
+    desc.componentFlags = 0;
+    desc.componentFlagsMask = 0;
+    desc.componentManufacturer = kAudioUnitManufacturer_Apple;
+    desc.componentType = kAudioUnitType_Output;
+
+#ifdef TARGET_IOS
+    desc.componentSubType = kAudioUnitSubType_RemoteIO;
+#else
+    desc.componentSubType = kAudioUnitSubType_DefaultOutput;
+#endif
+
+    err = AUGraphAddNode(mGraph, &desc, &mOutputNode);
+    CHECK_OSSTATUS(err);
+
+    struct AURenderCallbackStruct cb;
+    cb.inputProc = FeedInput;
+    cb.inputProcRefCon = this;
+
+    err = AUGraphSetNodeInputCallback(
+            mGraph, mOutputNode, 0 /* inputNumber */, &cb);
+
+    CHECK_OSSTATUS(err);
+
+    err = AUGraphOpen(mGraph);
+    CHECK_OSSTATUS(err);
+
+    AudioUnit outputUnit;
+    err = AUGraphNodeInfo(mGraph, mOutputNode, &desc, &outputUnit);
+    CHECK_OSSTATUS(err);
+
+    err = AudioUnitSetProperty(
+            outputUnit,
+            kAudioUnitProperty_StreamFormat,
+            kAudioUnitScope_Input,
+            0 /* busNumber */,
+            &mOutFormat,
+            sizeof(mOutFormat));
+
+    CHECK_OSSTATUS(err);
+
+    err = AUGraphInitialize(mGraph);
+    CHECK_OSSTATUS(err);
+
+    mBufferQueue.reset(
+            new BufferQueue(
+                8 /* count */,
+                mInFormat.mFramesPerPacket
+                    * mInFormat.mChannelsPerFrame * sizeof(float)));
+
+    err = AUGraphStart(mGraph);
+    CHECK_OSSTATUS(err);
+#else
+    err = AudioQueueNewOutput(
+            &mOutFormat,
+            PlayCallback,
+            this,
+            nullptr /* callbackRunLoop */,
+            kCFRunLoopCommonModes,
+            0 /* flags */,
+            &mQueue);
+
+    CHECK_OSSTATUS(err);
+
+    UInt32 enablePitch = 1;
+    err = AudioQueueSetProperty(
+            mQueue,
+            kAudioQueueProperty_EnableTimePitch,
+            &enablePitch,
+            sizeof(enablePitch));
+
+    CHECK_OSSTATUS(err);
+
+#if 0
+    UInt32 pitchAlgorithm = kAudioQueueTimePitchAlgorithm_Spectral;
+    err = AudioQueueSetProperty(
+            mQueue,
+            kAudioQueueProperty_TimePitchAlgorithm,
+            &pitchAlgorithm,
+            sizeof(pitchAlgorithm));
+
+    CHECK_OSSTATUS(err);
+#endif
+
+    err = AudioQueueSetParameter(mQueue, kAudioQueueParam_PlayRate, 1.0 /* 0.99 */);
+    CHECK_OSSTATUS(err);
+
+    err = AudioQueueAddPropertyListener(
+            mQueue,
+            kAudioQueueProperty_IsRunning,
+            PropertyListenerCallback,
+            this);
+
+    CHECK_OSSTATUS(err);
+
+    mBufferManager.reset(
+            new android::AudioQueueBufferManager(
+                mQueue,
+                32 /* count */,
+                mInFormat.mFramesPerPacket * channelCount * sizeof(float)));
+
+    err = AudioQueueStart(mQueue, nullptr /* startTime */);
+    CHECK_OSSTATUS(err);
+#endif
+
+    return OK;
+}
+
+#if !USE_AUDIO_UNIT
+// static
+void AACPlayer::PlayCallback(
+        void *cookie, AudioQueueRef /* queue */, AudioQueueBufferRef buffer) {
+    AACPlayer *me = static_cast<AACPlayer *>(cookie);
+    me->mBufferManager->release(buffer);
+}
+#endif
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/AACPlayer.h b/host/frontend/gcastv2/libsource/AACPlayer.h
new file mode 100644
index 0000000..59a52b8
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/AACPlayer.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include <source/AudioQueueBufferManager.h>
+#include <source/BufferQueue.h>
+
+#include <utils/Errors.h>
+
+#include <sys/types.h>
+
+#include <AudioToolbox/AudioToolbox.h>
+
+#include <memory>
+
+namespace android {
+
+#define USE_AUDIO_UNIT          1
+
+struct AACPlayer {
+    explicit AACPlayer();
+    ~AACPlayer();
+
+    AACPlayer(const AACPlayer &) = delete;
+    AACPlayer &operator=(const AACPlayer &) = delete;
+
+    status_t feedADTSFrame(const void *frame, size_t size);
+
+    int32_t sampleRateHz() const;
+
+private:
+    AudioStreamBasicDescription mInFormat, mOutFormat;
+    AudioConverterRef mConverter;
+
+#if USE_AUDIO_UNIT
+    AUGraph mGraph;
+    AUNode mOutputNode;
+    std::unique_ptr<BufferQueue> mBufferQueue;
+#else
+    AudioQueueRef mQueue;
+    std::unique_ptr<AudioQueueBufferManager> mBufferManager;
+#endif
+
+    int32_t mSampleRateHz;
+    int64_t mNumFramesSubmitted;
+
+    status_t init(int32_t sampleRate, size_t channelCount);
+
+#if !USE_AUDIO_UNIT
+    static void PlayCallback(
+            void *_cookie, AudioQueueRef queue, AudioQueueBufferRef buffer);
+#else
+    static OSStatus FeedInput(
+            void *cookie,
+            AudioUnitRenderActionFlags *flags,
+            const AudioTimeStamp *timeStamp,
+            UInt32 bus,
+            UInt32 numFrames,
+            AudioBufferList *data);
+#endif
+};
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libsource/Android.bp b/host/frontend/gcastv2/libsource/Android.bp
new file mode 100644
index 0000000..6876ede
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/Android.bp
@@ -0,0 +1,51 @@
+// Copyright (C) 2018 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.
+
+cc_library_host_static {
+    name: "libsource",
+    srcs: [
+        "AudioSource.cpp",
+        "CVMTouchSink.cpp",
+        "DisplayListsSource.cpp",
+        "FrameBufferSource.cpp",
+        "HostToGuestComms.cpp",
+        "StreamingSource.cpp",
+        "TouchSource.cpp",
+        "VSOCTouchSink.cpp",
+    ],
+    header_libs: [
+        "cuttlefish_glog",
+    ],
+    shared_libs: [
+        "libbase",
+        "libFraunhoferAAC",
+// "libx264",
+        "libyuv",
+        "libopus",
+        "libvpx",
+    ],
+    static_libs: [
+        "libandroidglue",
+        "libcuttlefish_host_config",
+        "libgflags",
+        "libhttps",
+    ],
+    cflags: [
+        "-DTARGET_ANDROID",
+    ],
+    local_include_dirs: ["include"],
+    export_include_dirs: ["include"],
+    defaults: ["cuttlefish_host_only"],
+}
+
diff --git a/host/frontend/gcastv2/libsource/AudioQueueBufferManager.cpp b/host/frontend/gcastv2/libsource/AudioQueueBufferManager.cpp
new file mode 100644
index 0000000..b1954c3
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/AudioQueueBufferManager.cpp
@@ -0,0 +1,85 @@
+#include <source/AudioQueueBufferManager.h>
+
+#include <media/stagefright/foundation/ALooper.h>
+
+namespace android {
+
+AudioQueueBufferManager::AudioQueueBufferManager(
+        AudioQueueRef queue, size_t count, size_t size)
+    : mInitCheck(NO_INIT),
+      mQueue(queue),
+      mBufferSize(size) {
+    for (size_t i = 0; i < count; ++i) {
+        AudioQueueBufferRef buffer;
+        OSStatus err = AudioQueueAllocateBuffer(mQueue, static_cast<UInt32>(size), &buffer);
+
+        if (err != noErr) {
+            mInitCheck = -ENOMEM;
+            return;
+        }
+
+        mBuffers.push_back(buffer);
+    }
+
+    mInitCheck = OK;
+}
+
+AudioQueueBufferManager::~AudioQueueBufferManager() {
+    for (auto buffer : mBuffers) {
+        AudioQueueFreeBuffer(mQueue, buffer);
+    }
+}
+
+status_t AudioQueueBufferManager::initCheck() const {
+    return mInitCheck;
+}
+
+size_t AudioQueueBufferManager::bufferSize() const {
+    return mBufferSize;
+}
+
+AudioQueueBufferRef AudioQueueBufferManager::acquire(int64_t timeoutUs) {
+    int64_t waitUntilUs =
+        (timeoutUs < 0ll) ? -1ll : ALooper::GetNowUs() + timeoutUs;
+
+    std::unique_lock<std::mutex> autoLock(mLock);
+    while (mBuffers.empty()) {
+        if (waitUntilUs < 0ll) {
+            mCondition.wait(autoLock);
+        } else {
+            int64_t nowUs = ALooper::GetNowUs();
+
+            if (nowUs >= waitUntilUs) {
+                break;
+            }
+
+            auto result = mCondition.wait_for(
+                    autoLock, std::chrono::microseconds(waitUntilUs - nowUs));
+
+            if (result == std::cv_status::timeout) {
+                break;
+            }
+        }
+    }
+
+    if (mBuffers.empty()) {
+        return nullptr;
+    }
+
+    auto result = mBuffers.front();
+    mBuffers.pop_front();
+
+    return result;
+}
+
+void AudioQueueBufferManager::release(AudioQueueBufferRef buffer) {
+    std::lock_guard<std::mutex> autoLock(mLock);
+    bool wasEmpty = mBuffers.empty();
+    mBuffers.push_back(buffer);
+    if (wasEmpty) {
+        mCondition.notify_all();
+    }
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/AudioSink.cpp b/host/frontend/gcastv2/libsource/AudioSink.cpp
new file mode 100644
index 0000000..f8fad19
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/AudioSink.cpp
@@ -0,0 +1,37 @@
+#include <source/AudioSink.h>
+
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/AMessage.h>
+
+namespace android {
+
+AudioSink::AudioSink()
+    : mAACPlayer(new AACPlayer) {
+#if LOG_AUDIO
+    mFile = fopen("/tmp/audio.aac", "w");
+    CHECK(mFile);
+#endif
+}
+
+void AudioSink::onMessageReceived(const sp<AMessage> &msg) {
+    switch (msg->what()) {
+        case kWhatAccessUnit:
+        {
+            sp<ABuffer> accessUnit;
+            CHECK(msg->findBuffer("accessUnit", &accessUnit));
+            mAACPlayer->feedADTSFrame(accessUnit->data(), accessUnit->size());
+
+#if LOG_AUDIO
+            fwrite(accessUnit->data(), 1, accessUnit->size(), mFile);
+            fflush(mFile);
+#endif
+            break;
+        }
+
+        default:
+            TRESPASS();
+    }
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/AudioSource.cpp b/host/frontend/gcastv2/libsource/AudioSource.cpp
new file mode 100644
index 0000000..9ea5e0d
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/AudioSource.cpp
@@ -0,0 +1,1097 @@
+#include <source/AudioSource.h>
+
+#include "common/vsoc/lib/circqueue_impl.h"
+#include "common/vsoc/lib/vsoc_audio_message.h"
+
+#include <media/stagefright/MediaDefs.h>
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/AMessage.h>
+#include <media/stagefright/foundation/TSPacketizer.h>
+#include <media/stagefright/foundation/hexdump.h>
+#include <libyuv/convert.h>
+
+#include "common/vsoc/lib/audio_data_region_view.h"
+#include "host/libs/config/cuttlefish_config.h"
+
+#include <aacenc_lib.h>
+
+#include <opus.h>
+
+#include <gflags/gflags.h>
+#include <cmath>
+
+#define LOG_AUDIO       0
+
+namespace android {
+
+struct AudioSource::Encoder {
+    explicit Encoder();
+    virtual ~Encoder() = default;
+
+    virtual status_t initCheck() const = 0;
+    virtual void encode(const void *data, size_t size) = 0;
+    virtual void reset() = 0;
+
+    void setFrameCallback(
+            std::function<void(const sp<ABuffer> &)> onFrameFn);
+
+protected:
+    std::function<void(const sp<ABuffer> &)> mOnFrameFn;
+};
+
+AudioSource::Encoder::Encoder()
+    : mOnFrameFn(nullptr) {
+}
+
+void AudioSource::Encoder::setFrameCallback(
+        std::function<void(const sp<ABuffer> &)> onFrameFn) {
+    mOnFrameFn = onFrameFn;
+}
+
+struct AudioSource::AACEncoder : public AudioSource::Encoder {
+    explicit AACEncoder(bool useADTSFraming);
+    ~AACEncoder() override;
+
+    status_t initCheck() const override;
+
+    AACEncoder(const AACEncoder &) = delete;
+    AACEncoder &operator=(const AACEncoder &) = delete;
+
+    void encode(const void *data, size_t size) override;
+    void reset() override;
+
+private:
+    static constexpr unsigned kAACProfile = AOT_AAC_LC;
+
+    status_t mInitCheck;
+
+    bool mUseADTSFraming;
+
+    gce_audio_message mPrevHeader;
+    bool mPrevHeaderValid;
+
+    HANDLE_AACENCODER mImpl;
+
+    sp<ABuffer> mConfig;
+    sp<ABuffer> mInputFrame;
+
+    size_t mADTSSampleRateIndex;
+    size_t mChannelCount;
+
+    FILE *mLogFile;
+
+    void fillADTSHeader(const sp<ABuffer> &outBuffer) const;
+    static bool GetSampleRateIndex(int32_t sampleRate, size_t *tableIndex);
+};
+
+AudioSource::AACEncoder::AACEncoder(bool useADTSFraming)
+    : mInitCheck(NO_INIT),
+      mUseADTSFraming(useADTSFraming),
+      mPrevHeaderValid(false),
+      mImpl(nullptr),
+      mADTSSampleRateIndex(0),
+      mChannelCount(0),
+      mLogFile(nullptr) {
+    reset();
+
+    if (mImpl != nullptr) {
+        mInitCheck = OK;
+    }
+}
+
+AudioSource::AACEncoder::~AACEncoder() {
+    if (mLogFile != nullptr) {
+        fclose(mLogFile);
+        mLogFile = nullptr;
+    }
+
+    if (mImpl != nullptr) {
+        aacEncClose(&mImpl);
+        mImpl = nullptr;
+    }
+}
+
+status_t AudioSource::AACEncoder::initCheck() const {
+    return mInitCheck;
+}
+
+void AudioSource::AACEncoder::reset() {
+    if (mLogFile != nullptr) {
+        fclose(mLogFile);
+        mLogFile = nullptr;
+    }
+
+#if LOG_AUDIO
+    mLogFile = fopen("/tmp/log_remote.aac", "wb");
+    CHECK(mLogFile != nullptr);
+#endif
+
+    if (mImpl != nullptr) {
+        aacEncClose(&mImpl);
+        mImpl = nullptr;
+    }
+
+    if (aacEncOpen(&mImpl, 0, 0) != AACENC_OK) {
+        mImpl = nullptr;
+        return;
+    }
+
+    mPrevHeaderValid = false;
+}
+
+void AudioSource::AACEncoder::encode(const void *_data, size_t size) {
+    auto data = static_cast<const uint8_t *>(_data);
+
+    CHECK_GE(size, sizeof(gce_audio_message));
+
+    gce_audio_message hdr;
+    std::memcpy(&hdr, data, sizeof(gce_audio_message));
+
+    if (hdr.message_type != gce_audio_message::DATA_SAMPLES) {
+        return;
+    }
+
+    int64_t timeUs =
+        static_cast<int64_t>(hdr.time_presented.tv_sec) * 1000000ll
+        + (hdr.time_presented.tv_nsec + 500) / 1000;
+
+    if (!mPrevHeaderValid
+            || mPrevHeader.frame_size != hdr.frame_size
+            || mPrevHeader.frame_rate != hdr.frame_rate
+            || mPrevHeader.stream_number != hdr.stream_number) {
+
+        if (mPrevHeaderValid) {
+            LOG(INFO) << "Found audio data in a different configuration than before!";
+
+            // reset?
+            return;
+        }
+
+        mPrevHeaderValid = true;
+        mPrevHeader = hdr;
+
+        CHECK_EQ(aacEncoder_SetParam(
+                    mImpl, AACENC_AOT, kAACProfile), AACENC_OK);
+
+        CHECK_EQ(aacEncoder_SetParam(
+                    mImpl, AACENC_SAMPLERATE, hdr.frame_rate), AACENC_OK);
+
+        CHECK_EQ(aacEncoder_SetParam(mImpl, AACENC_BITRATE, 128000), AACENC_OK);
+
+        const size_t numChannels = hdr.frame_size / sizeof(int16_t);
+        CHECK(numChannels == 1 || numChannels == 2);
+
+        mChannelCount = numChannels;
+
+        CHECK_EQ(aacEncoder_SetParam(
+                    mImpl,
+                    AACENC_CHANNELMODE,
+                    (numChannels == 1) ? MODE_1 : MODE_2),
+                AACENC_OK);
+
+        CHECK_EQ(aacEncoder_SetParam(
+                    mImpl, AACENC_TRANSMUX, TT_MP4_RAW), AACENC_OK);
+
+        CHECK_EQ(aacEncEncode(
+                    mImpl, nullptr, nullptr, nullptr, nullptr), AACENC_OK);
+
+        AACENC_InfoStruct encInfo;
+        CHECK_EQ(aacEncInfo(mImpl, &encInfo), AACENC_OK);
+
+        mConfig = new ABuffer(encInfo.confSize);
+        memcpy(mConfig->data(), encInfo.confBuf, encInfo.confSize);
+
+        // hexdump(mConfig->data(), mConfig->size(), 0, nullptr);
+
+        if (!mUseADTSFraming) {
+            if (mOnFrameFn) {
+                mOnFrameFn(mConfig);
+            }
+        } else {
+            CHECK(GetSampleRateIndex(hdr.frame_rate, &mADTSSampleRateIndex));
+        }
+
+        const size_t numBytesPerInputFrame =
+            numChannels * 1024 * sizeof(int16_t);
+
+        mInputFrame = new ABuffer(numBytesPerInputFrame);
+        mInputFrame->setRange(0, 0);
+    }
+
+    size_t offset = sizeof(gce_audio_message);
+    while (offset < size) {
+        if (mInputFrame->size() == 0) {
+            mInputFrame->meta()->setInt64("timeUs", timeUs);
+        }
+
+        size_t copy = std::min(
+                size - offset, mInputFrame->capacity() - mInputFrame->size());
+
+        memcpy(mInputFrame->data() + mInputFrame->size(), &data[offset], copy);
+        mInputFrame->setRange(0, mInputFrame->size() + copy);
+
+        offset += copy;
+
+        // "Time" on the input data has in effect advanced by the
+        // number of audio frames we just advanced offset by.
+        timeUs +=
+            (copy * 1000000ll / hdr.frame_rate) / (mChannelCount * sizeof(int16_t));
+
+        if (mInputFrame->size() == mInputFrame->capacity()) {
+            void *inBuffers[] = { nullptr };
+            INT inBufferIds[] = { IN_AUDIO_DATA };
+            INT inBufferSizes[] = { 0 };
+            INT inBufferElSizes[] = { sizeof(int16_t) };
+
+            AACENC_BufDesc inBufDesc;
+            inBufDesc.numBufs = sizeof(inBuffers) / sizeof(inBuffers[0]);
+            inBufDesc.bufs = inBuffers;
+            inBufDesc.bufferIdentifiers = inBufferIds;
+            inBufDesc.bufSizes = inBufferSizes;
+            inBufDesc.bufElSizes = inBufferElSizes;
+
+            static constexpr size_t kMaxFrameSize = 8192;
+            static constexpr size_t kADTSHeaderSize = 7;
+
+            sp<ABuffer> outBuffer =
+                new ABuffer(
+                        mUseADTSFraming
+                            ? kMaxFrameSize + kADTSHeaderSize : kMaxFrameSize);
+
+            if (mUseADTSFraming) {
+                outBuffer->setRange(0, kADTSHeaderSize);
+            } else {
+                outBuffer->setRange(0, 0);
+            }
+
+            void *outBuffers[] = { nullptr };
+            INT outBufferIds[] = { OUT_BITSTREAM_DATA };
+            INT outBufferSizes[] = { 0 };
+            INT outBufferElSizes[] = { sizeof(UCHAR) };
+
+            AACENC_BufDesc outBufDesc;
+            outBufDesc.numBufs = sizeof(outBuffers) / sizeof(outBuffers[0]);
+            outBufDesc.bufs = outBuffers;
+            outBufDesc.bufferIdentifiers = outBufferIds;
+            outBufDesc.bufSizes = outBufferSizes;
+            outBufDesc.bufElSizes = outBufferElSizes;
+
+            size_t inSampleOffset = 0;
+            do {
+                AACENC_InArgs inArgs;
+                AACENC_OutArgs outArgs;
+                memset(&inArgs, 0, sizeof(inArgs));
+                memset(&outArgs, 0, sizeof(outArgs));
+
+                inArgs.numInSamples =
+                    mInputFrame->size() / sizeof(int16_t) - inSampleOffset;
+
+                inBuffers[0] =
+                    mInputFrame->data() + inSampleOffset * sizeof(int16_t);
+
+                inBufferSizes[0] = inArgs.numInSamples * sizeof(int16_t);
+
+                outBuffers[0] = outBuffer->data() + outBuffer->size();
+                outBufferSizes[0] = outBuffer->capacity() - outBuffer->size();
+
+                CHECK_EQ(aacEncEncode(
+                            mImpl, &inBufDesc, &outBufDesc, &inArgs, &outArgs),
+                         AACENC_OK);
+
+                outBuffer->setRange(
+                        0, outBuffer->size() + outArgs.numOutBytes);
+
+                inSampleOffset += outArgs.numInSamples;
+            } while (inSampleOffset < (mInputFrame->size() / sizeof(int16_t)));
+
+            int64_t inputFrameTimeUs;
+            CHECK(mInputFrame->meta()->findInt64("timeUs", &inputFrameTimeUs));
+            outBuffer->meta()->setInt64("timeUs", inputFrameTimeUs);
+
+            mInputFrame->setRange(0, 0);
+
+            if (mUseADTSFraming) {
+                fillADTSHeader(outBuffer);
+            }
+
+#if LOG_AUDIO
+            fwrite(outBuffer->data(), 1, outBuffer->size(), mLogFile);
+            fflush(mLogFile);
+#endif
+
+            if (mOnFrameFn) {
+                mOnFrameFn(outBuffer);
+            }
+        }
+    }
+}
+
+void AudioSource::AACEncoder::fillADTSHeader(const sp<ABuffer> &outBuffer) const {
+    static constexpr unsigned kADTSId = 0;
+    static constexpr unsigned kADTSLayer = 0;
+    static constexpr unsigned kADTSProtectionAbsent = 1;
+
+    unsigned frameLength = outBuffer->size();
+    uint8_t *dst = outBuffer->data();
+
+    dst[0] = 0xff;
+
+    dst[1] =
+        0xf0 | (kADTSId << 3) | (kADTSLayer << 1) | kADTSProtectionAbsent;
+
+    dst[2] = ((kAACProfile - 1) << 6)
+            | (mADTSSampleRateIndex << 2)
+            | (mChannelCount >> 2);
+
+    dst[3] = ((mChannelCount & 3) << 6) | (frameLength >> 11);
+
+    dst[4] = (frameLength >> 3) & 0xff;
+    dst[5] = (frameLength & 7) << 5;
+    dst[6] = 0x00;
+}
+
+// static
+bool AudioSource::AACEncoder::GetSampleRateIndex(
+        int32_t sampleRate, size_t *tableIndex) {
+    static constexpr int32_t kSampleRateTable[] = {
+        96000, 88200, 64000, 48000, 44100, 32000,
+        24000, 22050, 16000, 12000, 11025, 8000
+    };
+    static constexpr size_t kNumSampleRates =
+        sizeof(kSampleRateTable) / sizeof(kSampleRateTable[0]);
+
+    *tableIndex = 0;
+    for (size_t index = 0; index < kNumSampleRates; ++index) {
+        if (sampleRate == kSampleRateTable[index]) {
+            *tableIndex = index;
+            return true;
+        }
+    }
+
+    return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct Upsampler {
+    explicit Upsampler(int32_t from = 44100, int32_t to = 48000)
+        : mFrom(from),
+          mTo(to),
+          mCounter(0) {
+    }
+
+    void append(const int16_t *data, size_t numFrames) {
+        for (size_t i = 0; i < numFrames; ++i) {
+            int16_t l = *data++;
+            int16_t r = *data++;
+
+            mCounter += mTo;
+            while (mCounter >= mFrom) {
+                mCounter -= mFrom;
+
+                mBuffer.push_back(l);
+                mBuffer.push_back(r);
+            }
+        }
+    }
+
+    const int16_t *data() const { return mBuffer.data(); }
+
+    size_t numFramesAvailable() const { return mBuffer.size() / 2; }
+
+    void drain(size_t numFrames) {
+        CHECK_LE(numFrames, numFramesAvailable());
+
+        mBuffer.erase(mBuffer.begin(), mBuffer.begin() + numFrames * 2);
+    }
+
+private:
+    int32_t mFrom;
+    int32_t mTo;
+
+    std::vector<int16_t> mBuffer;
+
+    int32_t mCounter;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct AudioSource::OPUSEncoder : public AudioSource::Encoder {
+    explicit OPUSEncoder();
+    ~OPUSEncoder() override;
+
+    status_t initCheck() const override;
+
+    OPUSEncoder(const OPUSEncoder &) = delete;
+    OPUSEncoder &operator=(const OPUSEncoder &) = delete;
+
+    void encode(const void *data, size_t size) override;
+    void reset() override;
+
+private:
+    status_t mInitCheck;
+
+    gce_audio_message mPrevHeader;
+    bool mPrevHeaderValid;
+
+    size_t mChannelCount;
+
+    OpusEncoder *mImpl;
+
+    std::unique_ptr<Upsampler> mUpSampler;
+
+    FILE *mLogFile;
+};
+
+AudioSource::OPUSEncoder::OPUSEncoder()
+    : mInitCheck(NO_INIT),
+      mImpl(nullptr),
+      mLogFile(nullptr) {
+    reset();
+    mInitCheck = OK;
+}
+
+AudioSource::OPUSEncoder::~OPUSEncoder() {
+    reset();
+}
+
+status_t AudioSource::OPUSEncoder::initCheck() const {
+    return mInitCheck;
+}
+
+void AudioSource::OPUSEncoder::reset() {
+    if (mLogFile != nullptr) {
+        fclose(mLogFile);
+        mLogFile = nullptr;
+    }
+
+    mUpSampler.reset();
+
+    if (mImpl) {
+        opus_encoder_destroy(mImpl);
+        mImpl = nullptr;
+    }
+
+    mPrevHeaderValid = false;
+    mChannelCount = 0;
+}
+
+void AudioSource::OPUSEncoder::encode(const void *_data, size_t size) {
+    auto data = static_cast<const uint8_t *>(_data);
+
+    CHECK_GE(size, sizeof(gce_audio_message));
+
+    gce_audio_message hdr;
+    std::memcpy(&hdr, data, sizeof(gce_audio_message));
+
+    if (hdr.message_type != gce_audio_message::DATA_SAMPLES) {
+        return;
+    }
+
+#if 0
+    int64_t timeUs =
+        static_cast<int64_t>(hdr.time_presented.tv_sec) * 1000000ll
+        + (hdr.time_presented.tv_nsec + 500) / 1000;
+#else
+    static int64_t timeUs = 0;
+#endif
+
+    static int64_t prevTimeUs = 0;
+
+    LOG(VERBOSE)
+        << "encode received "
+        << ((size - sizeof(gce_audio_message)) / (2 * sizeof(int16_t)))
+        << " frames, "
+        << " deltaTime = "
+        << (((timeUs - prevTimeUs) * hdr.frame_rate) / 1000000ll)
+        << " frames";
+
+    prevTimeUs = timeUs;
+
+    if (!mPrevHeaderValid
+            || mPrevHeader.frame_size != hdr.frame_size
+            || mPrevHeader.frame_rate != hdr.frame_rate
+            || mPrevHeader.stream_number != hdr.stream_number) {
+
+        if (mPrevHeaderValid) {
+            LOG(INFO)
+                << "Found audio data in a different configuration than before!"
+                << " frame_size="
+                << hdr.frame_size
+                << " vs. "
+                << mPrevHeader.frame_size
+                << ", frame_rate="
+                << hdr.frame_rate
+                << " vs. "
+                << mPrevHeader.frame_rate
+                << ", stream_number="
+                << hdr.stream_number
+                << " vs. "
+                << mPrevHeader.stream_number;
+
+            // reset?
+            return;
+        }
+
+        mPrevHeaderValid = true;
+        mPrevHeader = hdr;
+
+        const size_t numChannels = hdr.frame_size / sizeof(int16_t);
+
+#if LOG_AUDIO
+        mLogFile = fopen("/tmp/log_remote.opus", "wb");
+        CHECK(mLogFile != nullptr);
+#endif
+
+        LOG(INFO)
+            << "Calling opus_encoder_create w/ "
+            << "hdr.frame_rate = "
+            << hdr.frame_rate
+            << ", numChannels = "
+            << numChannels;
+
+        int err;
+        mImpl = opus_encoder_create(
+                48000,
+                numChannels,
+                OPUS_APPLICATION_AUDIO,
+                &err);
+
+        CHECK_EQ(err, OPUS_OK);
+
+        mChannelCount = numChannels;
+
+        static_assert(sizeof(int16_t) == sizeof(opus_int16));
+
+        err = opus_encoder_ctl(mImpl, OPUS_SET_INBAND_FEC(true));
+        CHECK_EQ(err, OPUS_OK);
+
+        err = opus_encoder_ctl(mImpl, OPUS_SET_PACKET_LOSS_PERC(10));
+        CHECK_EQ(err, OPUS_OK);
+
+        err = opus_encoder_ctl(
+                mImpl, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH_WIDEBAND));
+
+        CHECK_EQ(err, OPUS_OK);
+
+        CHECK_LE(hdr.frame_rate, 48000);
+        mUpSampler = std::make_unique<Upsampler>(hdr.frame_rate, 48000);
+    }
+
+    // {2.5, 5, 10, 20, 40, 60, 80, 100, 120} ms
+    static constexpr size_t kNumFramesPerOutputBuffer = 48 * 20;
+
+    const size_t offset = sizeof(gce_audio_message);
+    mUpSampler->append(
+            reinterpret_cast<const int16_t *>(&data[offset]),
+            (size - offset) / (mChannelCount * sizeof(int16_t)));
+
+    while (mUpSampler->numFramesAvailable() >= kNumFramesPerOutputBuffer) {
+        size_t copyFrames =
+            std::min(mUpSampler->numFramesAvailable(),
+                    kNumFramesPerOutputBuffer);
+
+        static constexpr size_t kMaxPacketSize = 8192;
+
+        sp<ABuffer> outBuffer = new ABuffer(kMaxPacketSize);
+
+        auto outSize = opus_encode(
+                mImpl,
+                reinterpret_cast<const opus_int16 *>(mUpSampler->data()),
+                copyFrames,
+                outBuffer->data(),
+                outBuffer->capacity());
+
+        CHECK_GT(outSize, 0);
+
+        outBuffer->setRange(0, outSize);
+
+        outBuffer->meta()->setInt64("timeUs", timeUs);
+
+        mUpSampler->drain(copyFrames);
+
+        timeUs += (copyFrames * 1000ll) / 48;
+
+#if LOG_AUDIO
+        fwrite(outBuffer->data(), 1, outBuffer->size(), mLogFile);
+        fflush(mLogFile);
+#endif
+
+        if (mOnFrameFn) {
+            mOnFrameFn(outBuffer);
+        }
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct Downsampler {
+    explicit Downsampler(int32_t from = 44100, int32_t to = 8000)
+        : mFrom(from),
+          mTo(to),
+          mCounter(0) {
+    }
+
+    void append(const int16_t *data, size_t numFrames) {
+        for (size_t i = 0; i < numFrames; ++i) {
+            int16_t l = *data++;
+            int16_t r = *data++;
+
+            mCounter += mTo;
+            if (mCounter >= mFrom) {
+                mCounter -= mFrom;
+
+                auto mono = (l + r) / 2;
+                mBuffer.push_back(mono);
+            }
+        }
+    }
+
+    const int16_t *data() const { return mBuffer.data(); }
+
+    size_t numFramesAvailable() const { return mBuffer.size(); }
+
+    void drain(size_t numFrames) {
+        CHECK_LE(numFrames, numFramesAvailable());
+
+        mBuffer.erase(mBuffer.begin(), mBuffer.begin() + numFrames);
+    }
+
+private:
+    int32_t mFrom;
+    int32_t mTo;
+
+    std::vector<int16_t> mBuffer;
+
+    int32_t mCounter;
+};
+
+struct AudioSource::G711Encoder : public AudioSource::Encoder {
+    enum class Mode {
+        ALAW,
+        ULAW,
+    };
+
+    explicit G711Encoder(Mode mode);
+
+    status_t initCheck() const override;
+
+    G711Encoder(const G711Encoder &) = delete;
+    G711Encoder &operator=(const G711Encoder &) = delete;
+
+    void encode(const void *data, size_t size) override;
+    void reset() override;
+
+private:
+    static constexpr size_t kNumFramesPerBuffer = 512;
+
+    status_t mInitCheck;
+    Mode mMode;
+
+    gce_audio_message mPrevHeader;
+    bool mPrevHeaderValid;
+
+    size_t mChannelCount;
+
+    sp<ABuffer> mOutputFrame;
+    Downsampler mDownSampler;
+
+    void doEncode(const int16_t *src, size_t numFrames);
+};
+
+AudioSource::G711Encoder::G711Encoder(Mode mode)
+    : mInitCheck(NO_INIT),
+      mMode(mode) {
+    reset();
+    mInitCheck = OK;
+}
+
+status_t AudioSource::G711Encoder::initCheck() const {
+    return mInitCheck;
+}
+
+void AudioSource::G711Encoder::reset() {
+    mPrevHeaderValid = false;
+    mChannelCount = 0;
+}
+
+void AudioSource::G711Encoder::encode(const void *_data, size_t size) {
+    auto data = static_cast<const uint8_t *>(_data);
+
+    CHECK_GE(size, sizeof(gce_audio_message));
+
+    gce_audio_message hdr;
+    std::memcpy(&hdr, data, sizeof(gce_audio_message));
+
+    if (hdr.message_type != gce_audio_message::DATA_SAMPLES) {
+        return;
+    }
+
+#if 0
+    int64_t timeUs =
+        static_cast<int64_t>(hdr.time_presented.tv_sec) * 1000000ll
+        + (hdr.time_presented.tv_nsec + 500) / 1000;
+#else
+    static int64_t timeUs = 0;
+#endif
+
+    static int64_t prevTimeUs = 0;
+
+    LOG(VERBOSE)
+        << "encode received "
+        << ((size - sizeof(gce_audio_message)) / (2 * sizeof(int16_t)))
+        << " frames, "
+        << " deltaTime = "
+        << ((timeUs - prevTimeUs) * 441) / 10000
+        << " frames";
+
+    prevTimeUs = timeUs;
+
+    if (!mPrevHeaderValid
+            || mPrevHeader.frame_size != hdr.frame_size
+            || mPrevHeader.frame_rate != hdr.frame_rate
+            || mPrevHeader.stream_number != hdr.stream_number) {
+
+        if (mPrevHeaderValid) {
+            LOG(INFO)
+                << "Found audio data in a different configuration than before!"
+                << " frame_size="
+                << hdr.frame_size
+                << " vs. "
+                << mPrevHeader.frame_size
+                << ", frame_rate="
+                << hdr.frame_rate
+                << " vs. "
+                << mPrevHeader.frame_rate
+                << ", stream_number="
+                << hdr.stream_number
+                << " vs. "
+                << mPrevHeader.stream_number;
+
+            // reset?
+            return;
+        }
+
+        mPrevHeaderValid = true;
+        mPrevHeader = hdr;
+
+        mChannelCount = hdr.frame_size / sizeof(int16_t);
+
+        // mono, 8-bit output samples.
+        mOutputFrame = new ABuffer(kNumFramesPerBuffer);
+    }
+
+    const size_t offset = sizeof(gce_audio_message);
+    mDownSampler.append(
+            reinterpret_cast<const int16_t *>(&data[offset]),
+            (size - offset) / (mChannelCount * sizeof(int16_t)));
+
+    while (mDownSampler.numFramesAvailable() >= kNumFramesPerBuffer) {
+        doEncode(mDownSampler.data(), kNumFramesPerBuffer);
+
+        mOutputFrame->meta()->setInt64("timeUs", timeUs);
+
+        mDownSampler.drain(kNumFramesPerBuffer);
+
+        timeUs += (kNumFramesPerBuffer * 1000ll) / 8;
+
+        if (mOnFrameFn) {
+            mOnFrameFn(mOutputFrame);
+        }
+    }
+}
+
+static unsigned clz16(uint16_t x) {
+    unsigned n = 0;
+    if ((x & 0xff00) == 0) {
+        n += 8;
+        x <<= 8;
+    }
+    if ((x & 0xf000) == 0) {
+        n += 4;
+        x <<= 4;
+    }
+
+    static const unsigned kClzNibble[] = {
+        4, 3, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0
+    };
+
+    return n + kClzNibble[n >> 12];
+}
+
+void AudioSource::G711Encoder::doEncode(const int16_t *src, size_t numFrames) {
+    switch (mMode) {
+        case Mode::ALAW:
+        {
+            uint8_t *dst = mOutputFrame->data();
+
+            for (size_t i = numFrames; i--;) {
+                uint16_t in = (*src++) >> 3;  // Convert from 16-bit to 13-bit.
+                uint8_t inverseSign = 0x80;
+
+                if (in & 0x8000) {
+                    in = ~in;
+                    inverseSign = 0x00;
+                }
+
+                auto numLeadingZeroes = clz16(in);
+                auto suffixLength = 16 - numLeadingZeroes;
+
+                static constexpr uint8_t kMask = 0x55;
+
+                if (suffixLength <= 5) {
+                    *dst++ = (((in >> 1) & 0x0f) | inverseSign) ^ kMask;
+                } else {
+                    auto shift = suffixLength - 5;
+                    auto abcd = (in >> shift) & 0x0f;
+                    *dst++ = (abcd | (shift << 4) | inverseSign) ^ kMask;
+                }
+            }
+            break;
+        }
+
+        case Mode::ULAW:
+        {
+            uint8_t *dst = mOutputFrame->data();
+
+            for (size_t i = numFrames; i--;) {
+                uint16_t in = (*src++) >> 2;  // Convert from 16-bit to 14-bit.
+                uint8_t inverseSign = 0x80;
+
+                if (in & 0x8000) {
+                    in = ~in;
+                    inverseSign = 0x00;
+                }
+
+                in += 33;
+
+                auto numLeadingZeroes = clz16(in);
+                auto suffixLength = 16 - numLeadingZeroes;
+
+                static constexpr uint8_t kMask = 0xff;
+
+                if (suffixLength <= 6) {
+                    *dst++ = (((in >> 1) & 0x0f) | inverseSign) ^ kMask;
+                } else {
+                    auto shift = suffixLength - 5;
+                    auto abcd = (in >> shift) & 0x0f;
+                    *dst++ = (abcd | ((shift - 1) << 4) | inverseSign) ^ kMask;
+                }
+            }
+            break;
+        }
+
+        default:
+            TRESPASS();
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+AudioSource::AudioSource(Format format, bool useADTSFraming)
+    : mInitCheck(NO_INIT),
+      mState(STOPPED),
+      mRegionView(nullptr)
+#if SIMULATE_AUDIO
+      ,mPhase(0)
+#endif
+{
+    auto config = vsoc::CuttlefishConfig::Get();
+    if (config->enable_ivserver()) {
+        mRegionView = AudioDataRegionView::GetInstance(vsoc::GetDomain().c_str());
+        mRegionWorker = mRegionView->StartWorker();
+    }
+
+    switch (format) {
+        case Format::AAC:
+        {
+            mEncoder.reset(new AACEncoder(useADTSFraming));
+            break;
+        }
+
+        case Format::OPUS:
+        {
+            CHECK(!useADTSFraming);
+            mEncoder.reset(new OPUSEncoder);
+            break;
+        }
+
+        case Format::G711_ALAW:
+        case Format::G711_ULAW:
+        {
+            CHECK(!useADTSFraming);
+
+            mEncoder.reset(
+                    new G711Encoder(
+                        (format == Format::G711_ALAW)
+                            ? G711Encoder::Mode::ALAW
+                            : G711Encoder::Mode::ULAW));
+            break;
+        }
+
+        default:
+            TRESPASS();
+    }
+
+    mEncoder->setFrameCallback([this](const sp<ABuffer> &accessUnit) {
+        StreamingSource::onAccessUnit(accessUnit);
+    });
+
+    mInitCheck = OK;
+}
+
+AudioSource::~AudioSource() {
+    stop();
+}
+
+status_t AudioSource::initCheck() const {
+    return mInitCheck;
+}
+
+sp<AMessage> AudioSource::getFormat() const {
+    return mFormat;
+}
+
+status_t AudioSource::start() {
+    std::lock_guard<std::mutex> autoLock(mLock);
+
+    if (mState != STOPPED) {
+        return OK;
+    }
+
+    mEncoder->reset();
+
+    mState = RUNNING;
+
+#if SIMULATE_AUDIO
+    mThread.reset(
+            new std::thread([this]{
+                auto startTime = std::chrono::steady_clock::now();
+
+                std::vector<uint8_t> raw(
+                        sizeof(gce_audio_message)
+                            + kNumFramesPerBuffer * kNumChannels * sizeof(int16_t));
+
+                gce_audio_message *buffer =
+                        reinterpret_cast<gce_audio_message *>(raw.data());
+
+                buffer->message_type = gce_audio_message::DATA_SAMPLES;
+                buffer->frame_size = kNumChannels * sizeof(int16_t);
+                buffer->frame_rate = kSampleRate;
+                buffer->stream_number = 0;
+
+                const double k = (double)kFrequency / kSampleRate * (2.0 * M_PI);
+
+                while (mState != STOPPING) {
+                    std::chrono::microseconds durationSinceStart(
+                            (mPhase  * 1000000ll) / kSampleRate);
+
+                    auto time = startTime + durationSinceStart;
+                    auto now = std::chrono::steady_clock::now();
+                    auto delayUs = std::chrono::duration_cast<
+                            std::chrono::microseconds>(time - now).count();
+
+                    if (delayUs > 0) {
+                        usleep(delayUs);
+                    }
+
+                    auto usSinceStart =
+                        std::chrono::duration_cast<std::chrono::microseconds>(
+                                std::chrono::steady_clock::now() - startTime).count();
+
+                    buffer->time_presented.tv_sec = usSinceStart / 1000000ll;
+
+                    buffer->time_presented.tv_nsec =
+                        (usSinceStart % 1000000ll) * 1000;
+
+                    int16_t *ptr =
+                        reinterpret_cast<int16_t *>(
+                                raw.data() + sizeof(gce_audio_message));
+
+                    double x = mPhase * k;
+                    for (size_t i = 0; i < kNumFramesPerBuffer; ++i) {
+                        int16_t amplitude = (int16_t)(32767.0 * sin(x));
+
+                        *ptr++ = amplitude;
+                        if (kNumChannels == 2) {
+                            *ptr++ = amplitude;
+                        }
+
+                        x += k;
+                    }
+
+                    mEncoder->encode(raw.data(), raw.size());
+
+                    mPhase += kNumFramesPerBuffer;
+                }
+            }));
+#else
+    if (mRegionView) {
+        mThread.reset(
+                new std::thread([this]{
+                    while (mState != STOPPING) {
+                        uint8_t buffer[4096];
+
+                        struct timespec absTimeLimit;
+                        vsoc::RegionView::GetFutureTime(
+                                1000000000ll /* ns_from_now */, &absTimeLimit);
+
+                        intptr_t res = mRegionView->data()->audio_queue.Read(
+                                mRegionView,
+                                reinterpret_cast<char *>(buffer),
+                                sizeof(buffer),
+                                &absTimeLimit);
+
+                        if (res < 0) {
+                            if (res == -ETIMEDOUT) {
+                                LOG(VERBOSE) << "AudioSource read timed out";
+                            }
+                            continue;
+                        }
+
+                        if (mState == RUNNING) {
+                            mEncoder->encode(buffer, static_cast<size_t>(res));
+                        }
+                    }
+            }));
+    }
+#endif  // SIMULATE_AUDIO
+
+    return OK;
+}
+
+status_t AudioSource::stop() {
+    std::lock_guard<std::mutex> autoLock(mLock);
+
+    if (mState == STOPPED) {
+        return OK;
+    }
+
+    mState = STOPPING;
+
+    if (mThread) {
+        mThread->join();
+        mThread.reset();
+    }
+
+    mState = STOPPED;
+
+    return OK;
+}
+
+status_t AudioSource::requestIDRFrame() {
+    return OK;
+}
+
+void AudioSource::inject(const void *data, size_t size) {
+    // Only used in the case of CrosVM operation.
+
+    std::lock_guard<std::mutex> autoLock(mLock);
+    if (mState != State::RUNNING) {
+        return;
+    }
+
+    mEncoder->encode(static_cast<const uint8_t *>(data), size);
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libsource/BufferQueue.cpp b/host/frontend/gcastv2/libsource/BufferQueue.cpp
new file mode 100644
index 0000000..ae1a121
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/BufferQueue.cpp
@@ -0,0 +1,111 @@
+#include <source/BufferQueue.h>
+
+#include <media/stagefright/foundation/ALooper.h>
+
+namespace android {
+
+BufferQueue::BufferQueue(size_t count, size_t size)
+    : mInitCheck(NO_INIT),
+      mBufferSize(size) {
+    for (size_t i = 0; i < count; ++i) {
+        void *buffer = malloc(size);
+        if (buffer == nullptr) {
+            mInitCheck = -ENOMEM;
+            return;
+        }
+
+        mEmptyBuffers.push_back(buffer);
+    }
+
+    mInitCheck = OK;
+}
+
+BufferQueue::~BufferQueue() {
+    for (auto buffer : mEmptyBuffers) {
+        free(buffer);
+    }
+}
+
+status_t BufferQueue::initCheck() const {
+    return mInitCheck;
+}
+
+size_t BufferQueue::bufferSize() const {
+    return mBufferSize;
+}
+
+void *BufferQueue::acquire(int64_t timeoutUs) {
+    int64_t waitUntilUs =
+        (timeoutUs < 0ll) ? -1ll : ALooper::GetNowUs() + timeoutUs;
+
+    std::unique_lock<std::mutex> autoLock(mLock);
+    while (mEmptyBuffers.empty()) {
+        if (waitUntilUs < 0ll) {
+            mCondition.wait(autoLock);
+        } else {
+            int64_t nowUs = ALooper::GetNowUs();
+
+            if (nowUs >= waitUntilUs) {
+                break;
+            }
+
+            auto result = mCondition.wait_for(
+                    autoLock, std::chrono::microseconds(waitUntilUs - nowUs));
+
+            if (result == std::cv_status::timeout) {
+                break;
+            }
+        }
+    }
+
+    if (mEmptyBuffers.empty()) {
+        return nullptr;
+    }
+
+    auto result = mEmptyBuffers.front();
+    mEmptyBuffers.pop_front();
+
+    return result;
+}
+
+void BufferQueue::queue(void *data) {
+    std::lock_guard<std::mutex> autoLock(mLock);
+    bool wasEmpty = mFullBuffers.empty();
+    mFullBuffers.push_back(Buffer { data, 0 /* offset */ });
+    if (wasEmpty) {
+        mCondition.notify_all();
+    }
+}
+
+void *BufferQueue::dequeueBegin(size_t *size) {
+    std::lock_guard<std::mutex> autoLock(mLock);
+    if (mFullBuffers.empty()) {
+        return nullptr;
+    }
+
+    Buffer &result = mFullBuffers.front();
+    *size = mBufferSize - result.mOffset;
+
+    return static_cast<uint8_t *>(result.mData) + result.mOffset;
+}
+
+void BufferQueue::dequeueEnd(size_t size) {
+    std::lock_guard<std::mutex> autoLock(mLock);
+    CHECK(!mFullBuffers.empty());
+    Buffer &result = mFullBuffers.front();
+    CHECK_LE(size, mBufferSize - result.mOffset);
+    result.mOffset = mBufferSize - size;
+    if (size == 0) {
+        bool wasEmpty = mEmptyBuffers.empty();
+        mEmptyBuffers.push_back(result.mData);
+
+        if (wasEmpty) {
+            mCondition.notify_all();
+        }
+
+        mFullBuffers.pop_front();
+    }
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/CVMTouchSink.cpp b/host/frontend/gcastv2/libsource/CVMTouchSink.cpp
new file mode 100644
index 0000000..6d3712b
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/CVMTouchSink.cpp
@@ -0,0 +1,229 @@
+#include <source/CVMTouchSink.h>
+
+#include <https/SafeCallbackable.h>
+#include <https/Support.h>
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/AMessage.h>
+
+#include <sys/socket.h>
+
+namespace android {
+
+CVMTouchSink::CVMTouchSink(std::shared_ptr<RunLoop> runLoop, int serverFd)
+    : mRunLoop(runLoop),
+      mServerFd(serverFd),
+      mClientFd(-1),
+      mSendPending(false) {
+    if (mServerFd >= 0) {
+        makeFdNonblocking(mServerFd);
+    }
+}
+
+CVMTouchSink::~CVMTouchSink() {
+    if (mClientFd >= 0) {
+        mRunLoop->cancelSocket(mClientFd);
+
+        close(mClientFd);
+        mClientFd = -1;
+    }
+
+    if (mServerFd >= 0) {
+        mRunLoop->cancelSocket(mServerFd);
+
+        close(mServerFd);
+        mServerFd = -1;
+    }
+}
+
+void CVMTouchSink::start() {
+    if (mServerFd < 0) {
+        return;
+    }
+
+    mRunLoop->postSocketRecv(
+            mServerFd,
+            makeSafeCallback(this, &CVMTouchSink::onServerConnection));
+}
+
+void CVMTouchSink::onServerConnection() {
+    int s = accept(mServerFd, nullptr, nullptr);
+
+    if (s >= 0) {
+        if (mClientFd >= 0) {
+            LOG(INFO) << "Rejecting client, we already have one.";
+
+            // We already have a client.
+            close(s);
+            s = -1;
+        } else {
+            LOG(INFO) << "Accepted client socket " << s << ".";
+
+            makeFdNonblocking(s);
+
+            mClientFd = s;
+        }
+    }
+
+    mRunLoop->postSocketRecv(
+            mServerFd,
+            makeSafeCallback(this, &CVMTouchSink::onServerConnection));
+}
+
+static void AddInputEvent(
+        std::vector<input_event> *events,
+        uint16_t type,
+        uint16_t code,
+        int32_t value) {
+    input_event ev;
+    ev.type = type;
+    ev.code = code;
+    ev.value = value;
+
+    events->push_back(ev);
+}
+
+void CVMTouchSink::onAccessUnit(const sp<ABuffer> &accessUnit) {
+    const int32_t *data =
+        reinterpret_cast<const int32_t *>(accessUnit->data());
+
+    if (accessUnit->size() == 3 * sizeof(int32_t)) {
+        // Legacy: Single Touch Emulation.
+
+        bool down = data[0] != 0;
+        int x = data[1];
+        int y = data[2];
+
+        LOG(VERBOSE)
+            << "Received touch (down="
+            << down
+            << ", x="
+            << x
+            << ", y="
+            << y;
+
+        std::vector<input_event> events;
+        AddInputEvent(&events, EV_ABS, ABS_X, x);
+        AddInputEvent(&events, EV_ABS, ABS_Y, y);
+        AddInputEvent(&events, EV_KEY, BTN_TOUCH, down);
+        AddInputEvent(&events, EV_SYN, 0, 0);
+
+        sendEvents(events);
+        return;
+    }
+
+    CHECK_EQ(accessUnit->size(), 5 * sizeof(int32_t));
+
+    int id = data[0];
+    bool initialDown = data[1] != 0;
+    int x = data[2];
+    int y = data[3];
+    int slot = data[4];
+
+    LOG(VERBOSE)
+        << "Received touch (id="
+        << id
+        << ", initialDown="
+        << initialDown
+        << ", x="
+        << x
+        << ", y="
+        << y
+        << ", slot="
+        << slot;
+
+    std::vector<input_event> events;
+
+#if 0
+    AddInputEvent(&events, EV_ABS, ABS_MT_SLOT, slot);
+
+    if (id < 0 || initialDown) {
+        AddInputEvent(&events, EV_ABS, ABS_MT_TRACKING_ID, id);
+        AddInputEvent(&events, EV_KEY, BTN_TOUCH, initialDown);
+    }
+
+    if (id >= 0) {
+        AddInputEvent(&events, EV_ABS, ABS_MT_POSITION_X, x);
+        AddInputEvent(&events, EV_ABS, ABS_MT_POSITION_Y, y);
+    }
+
+    AddInputEvent(&events, EV_SYN, SYN_REPORT, 0);
+#else
+    AddInputEvent(&events, EV_ABS, ABS_X, x);
+    AddInputEvent(&events, EV_ABS, ABS_Y, y);
+    AddInputEvent(&events, EV_KEY, BTN_TOUCH, id >= 0);
+    AddInputEvent(&events, EV_SYN, 0, 0);
+#endif
+
+    sendEvents(events);
+}
+
+void CVMTouchSink::sendEvents(const std::vector<input_event> &events) {
+    if (events.empty()) {
+        return;
+    }
+
+    std::lock_guard autoLock(mLock);
+
+    if (mClientFd < 0) {
+        return;
+    }
+
+    auto size = events.size() * sizeof(input_event);
+
+    size_t offset = mOutBuffer.size();
+    mOutBuffer.resize(offset + size);
+    memcpy(mOutBuffer.data() + offset, events.data(), size);
+
+    if (!mSendPending) {
+        mSendPending = true;
+
+        mRunLoop->postSocketSend(
+                mClientFd,
+                makeSafeCallback(this, &CVMTouchSink::onSocketSend));
+    }
+}
+
+void CVMTouchSink::onSocketSend() {
+    std::lock_guard autoLock(mLock);
+
+    CHECK(mSendPending);
+    mSendPending = false;
+
+    if (mClientFd < 0) {
+        return;
+    }
+
+    ssize_t n;
+    while (!mOutBuffer.empty()) {
+        do {
+            n = ::send(mClientFd, mOutBuffer.data(), mOutBuffer.size(), 0);
+        } while (n < 0 && errno == EINTR);
+
+        if (n <= 0) {
+            break;
+        }
+
+        mOutBuffer.erase(mOutBuffer.begin(), mOutBuffer.begin() + n);
+    }
+
+    if ((n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) || n == 0) {
+        LOG(ERROR) << "Client is gone.";
+
+        // Client is gone.
+        mRunLoop->cancelSocket(mClientFd);
+
+        close(mClientFd);
+        mClientFd = -1;
+        return;
+    }
+
+    if (!mOutBuffer.empty()) {
+        mSendPending = true;
+        mRunLoop->postSocketSend(
+                mClientFd,
+                makeSafeCallback(this, &CVMTouchSink::onSocketSend));
+    }
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/CameraSource.cpp b/host/frontend/gcastv2/libsource/CameraSource.cpp
new file mode 100644
index 0000000..55e0755
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/CameraSource.cpp
@@ -0,0 +1,185 @@
+#include <source/CameraSource.h>
+
+#include <media/stagefright/MediaDefs.h>
+#include <media/stagefright/avc_utils.h>
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/AMessage.h>
+#include <media/stagefright/foundation/TSPacketizer.h>
+
+namespace android {
+
+CameraSource::CameraSource()
+    : mInitCheck(NO_INIT),
+      mState(STOPPED),
+      mSession(createCameraSession(&CameraSource::onFrameData, this)) {
+    mInitCheck = OK;
+}
+
+CameraSource::~CameraSource() {
+    stop();
+
+    destroyCameraSession(mSession);
+    mSession = nullptr;
+}
+
+status_t CameraSource::initCheck() const {
+    return mInitCheck;
+}
+
+sp<AMessage> CameraSource::getFormat() const {
+    return nullptr;
+}
+
+status_t CameraSource::start() {
+    std::lock_guard<std::mutex> autoLock(mLock);
+
+    if (mState != STOPPED) {
+        return OK;
+    }
+
+    mState = RUNNING;
+    startCameraSession(mSession);
+
+    return OK;
+}
+
+status_t CameraSource::stop() {
+    std::lock_guard<std::mutex> autoLock(mLock);
+
+    if (mState == STOPPED) {
+        return OK;
+    }
+
+    mState = STOPPED;
+    stopCameraSession(mSession);
+
+    return OK;
+}
+
+status_t CameraSource::pause() {
+    std::lock_guard<std::mutex> autoLock(mLock);
+
+    if (mState == PAUSED) {
+        return OK;
+    }
+
+    if (mState != RUNNING) {
+        return INVALID_OPERATION;
+    }
+
+    mState = PAUSED;
+    pauseCameraSession(mSession);
+
+    ALOGI("Now paused.");
+
+    return OK;
+}
+
+status_t CameraSource::resume() {
+    std::lock_guard<std::mutex> autoLock(mLock);
+
+    if (mState == RUNNING) {
+        return OK;
+    }
+
+    if (mState != PAUSED) {
+        return INVALID_OPERATION;
+    }
+
+    mState = RUNNING;
+    resumeCameraSession(mSession);
+
+    ALOGI("Now running.");
+
+    return OK;
+}
+
+bool CameraSource::paused() const {
+    return mState == PAUSED;
+}
+
+status_t CameraSource::requestIDRFrame() {
+    return OK;
+}
+
+// static
+void CameraSource::onFrameData(
+        void *cookie,
+        ssize_t csdIndex,
+        int64_t timeUs,
+        const void *data,
+        size_t size) {
+    return static_cast<CameraSource *>(cookie)->onFrameData(
+            csdIndex, timeUs, data, size);
+}
+
+static uint32_t U32BE_AT(const void *_data) {
+    const uint8_t *data = (const uint8_t *)_data;
+    return data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3];
+}
+
+void CameraSource::onFrameData(
+        ssize_t csdIndex, int64_t timeUs, const void *_data, size_t size) {
+    const uint8_t *data = static_cast<const uint8_t *>(_data);
+
+    ALOGV("got frame data csdIndex=%zd at %lld us, data %p, size %zu",
+          csdIndex, timeUs, data, size);
+
+    if (csdIndex >= 0) {
+        sp<ABuffer> csd = new ABuffer(4 + size);
+        memcpy(csd->data(), "\x00\x00\x00\x01", 4);
+        memcpy(csd->data() + 4, data, size);
+
+        mCSD.push_back(csd);
+        return;
+    }
+
+    sp<ABuffer> accessUnit = new ABuffer(size);
+    memcpy(accessUnit->data(), data, size);
+
+    size_t offset = 0;
+    while (offset + 3 < size) {
+        uint32_t naluLength = U32BE_AT(&data[offset]);
+        CHECK_LE(offset + 4 + naluLength, size);
+
+        memcpy(accessUnit->data() + offset, "\x00\x00\x00\x01", 4);
+
+        offset += 4;
+        // ALOGI("nalType 0x%02x", data[offset] & 0x1f);
+
+        CHECK_GT(naluLength, 0u);
+        offset += naluLength;
+    }
+    CHECK_EQ(offset, size);
+
+    if (IsIDR(accessUnit)) {
+        accessUnit = prependCSD(accessUnit);
+    }
+
+    accessUnit->meta()->setInt64("timeUs", timeUs);
+
+    sp<AMessage> notify = mNotify->dup();
+    notify->setBuffer("accessUnit", accessUnit);
+    notify->post();
+}
+
+sp<ABuffer> CameraSource::prependCSD(const sp<ABuffer> &accessUnit) const {
+    size_t size = 0;
+    for (const auto &csd : mCSD) {
+        size += csd->size();
+    }
+
+    sp<ABuffer> dup = new ABuffer(accessUnit->size() + size);
+    size_t offset = 0;
+    for (const auto &csd : mCSD) {
+        memcpy(dup->data() + offset, csd->data(), csd->size());
+        offset += csd->size();
+    }
+
+    memcpy(dup->data() + offset, accessUnit->data(), accessUnit->size());
+
+    return dup;
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libsource/CameraSourceSupport.h b/host/frontend/gcastv2/libsource/CameraSourceSupport.h
new file mode 100644
index 0000000..9005921
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/CameraSourceSupport.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#import <AVFoundation/AVFoundation.h>
+#import <Foundation/Foundation.h>
+#import <VideoToolbox/VideoToolbox.h>
+
+typedef void (*CameraSessionCallback)(
+        void *cookie,
+        ssize_t csdIndex,
+        int64_t timeUs,
+        const void *data,
+        size_t size);
+
+@interface MyVideoOutputDelegate
+    : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate> {
+    BOOL first_;
+
+    CameraSessionCallback cb_;
+    void *cookie_;
+    BOOL paused_;
+
+#ifdef TARGET_IOS
+    VTCompressionSessionRef compressionSession_;
+#endif
+}
+
+-(id)initWithCallback:(CameraSessionCallback)cb cookie:(void *)cookie;
+-(void)pause;
+-(void)resume;
+
+@end
+
+@interface MyCameraSession : NSObject {
+    AVCaptureSession *session_;
+    AVCaptureDevice *camera_;
+    dispatch_queue_t dispatchQueue_;
+    MyVideoOutputDelegate *delegate_;
+}
+
+-(id)initWithCallback:(CameraSessionCallback)cb cookie:(void *)cookie;
+-(void)pause;
+-(void)resume;
+
+@end
+
diff --git a/host/frontend/gcastv2/libsource/CameraSourceSupport.m b/host/frontend/gcastv2/libsource/CameraSourceSupport.m
new file mode 100644
index 0000000..5835935
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/CameraSourceSupport.m
@@ -0,0 +1,270 @@
+#import "CameraSourceSupport.h"
+
+@implementation MyVideoOutputDelegate
+
+-(id)initWithCallback:(CameraSessionCallback)cb cookie:(void *)cookie {
+    if ((self = [super init]) != nil) {
+        first_ = YES;
+
+        cb_ = cb;
+        cookie_ = cookie;
+
+        paused_ = false;
+
+#ifdef TARGET_IOS
+        compressionSession_ = nil;
+#endif
+    }
+
+    return self;
+}
+
+-(void)pause {
+    paused_ = true;
+}
+
+-(void)resume {
+    paused_ = false;
+}
+
+-(void)onCompressedFrame:(CMSampleBufferRef)buffer {
+    if (first_) {
+        first_ = NO;
+
+        CMFormatDescriptionRef format =
+            CMSampleBufferGetFormatDescription(buffer);
+
+        size_t numParameterSets;
+        OSStatus err = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
+                format, 0 /* index */, NULL, NULL, &numParameterSets, NULL);
+        NSAssert(
+                err == noErr,
+                @"CMVideoFormatDescriptionGetH264ParameterSetAtIndex failed");
+
+        const uint8_t *params;
+        size_t paramsSize;
+        for (size_t i = 0; i < numParameterSets; ++i) {
+            err = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
+                    format, i, &params, &paramsSize, NULL, NULL);
+            NSAssert(
+                    err == noErr,
+                    @"CMVideoFormatDescriptionGetH264ParameterSetAtIndex failed");
+
+            cb_(cookie_, i, 0ll, params, paramsSize);
+        }
+    }
+
+    CMItemCount numSamples = CMSampleBufferGetNumSamples(buffer);
+    NSAssert(numSamples == 1, @"expected a single sample");
+
+    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(buffer);
+
+    size_t sampleSize = CMSampleBufferGetSampleSize(buffer, 0 /* index */);
+
+    size_t lengthAtOffset;
+    size_t totalLength;
+    char *ptr;
+    OSStatus err = CMBlockBufferGetDataPointer(
+            blockBuffer, 0 /* offset */, &lengthAtOffset, &totalLength, &ptr);
+    NSAssert(err == kCMBlockBufferNoErr, @"CMBlockBufferGetDataPointer failed");
+    NSAssert(lengthAtOffset == sampleSize, @"sampleSize mismatch");
+    NSAssert(lengthAtOffset <= totalLength, @"totalLength mismatch");
+
+    // NSLog(@"  sample has size %zu, ptr:%p", sampleSize, ptr);
+
+    CMSampleTimingInfo info;
+    err = CMSampleBufferGetSampleTimingInfo(buffer, 0 /* sampleIndex */, &info);
+    NSAssert(err == noErr, @"CMSampleBufferGetSampleTimingInfo failed");
+
+    const int64_t timeUs =
+        (int64_t)(CMTimeGetSeconds(info.presentationTimeStamp) * 1e6);
+
+    cb_(cookie_, -1, timeUs, ptr, sampleSize);
+}
+
+#ifdef TARGET_IOS
+-(void)dealloc {
+    VTCompressionSessionInvalidate(compressionSession_);
+    compressionSession_ = nil;
+}
+
+void CompressionCallback(
+        void *cookie,
+        void *sourceFrameRefCon,
+        OSStatus status,
+        VTEncodeInfoFlags infoFlags,
+        CMSampleBufferRef buffer) {
+    [(__bridge MyVideoOutputDelegate *)cookie onCompressedFrame:buffer];
+}
+
+-(void)captureOutput:(AVCaptureOutput *)output
+       didOutputSampleBuffer:(CMSampleBufferRef)buffer
+       fromConnection:(AVCaptureConnection *)connection {
+    if (paused_) {
+        return;
+    }
+    
+    CMFormatDescriptionRef format =
+        CMSampleBufferGetFormatDescription(buffer);
+
+    if (compressionSession_ == nil) {
+        CMVideoDimensions dim = CMVideoFormatDescriptionGetDimensions(format);
+
+        OSStatus err = VTCompressionSessionCreate(
+                kCFAllocatorDefault,
+                dim.width,
+                dim.height,
+                kCMVideoCodecType_H264,
+                NULL /* encoderSettings */,
+                NULL /* sourceImageBufferAttributes */,
+                kCFAllocatorDefault,
+                CompressionCallback,
+                (__bridge void *)self,
+                &compressionSession_);
+
+        NSAssert(err == noErr, @"VTCompressionSessionCreate failed");
+    }
+
+    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(buffer);
+
+    CMSampleTimingInfo info;
+    OSStatus err = CMSampleBufferGetSampleTimingInfo(buffer, 0 /* sampleIndex */, &info);
+    NSAssert(err == noErr, @"CMSampleBufferGetSampleTimingInfo failed");
+
+    VTEncodeInfoFlags infoFlags;
+    err = VTCompressionSessionEncodeFrame(
+            compressionSession_,
+            imageBuffer,
+            info.presentationTimeStamp,
+            info.duration,
+            NULL /* frameProperties */,
+            NULL /* sourceFrameRefCon */,
+            &infoFlags);
+
+    NSAssert(err == noErr, @"VTCompressionSessionEncodeFrame failed");
+}
+#else
+-(void)captureOutput:(AVCaptureOutput *)output
+       didOutputSampleBuffer:(CMSampleBufferRef)buffer
+       fromConnection:(AVCaptureConnection *)connection {
+    [self onCompressedFrame:buffer];
+}
+#endif
+
+-(void)captureOutput:(AVCaptureOutput *)output
+ didDropSampleBuffer:(CMSampleBufferRef)buffer
+ fromConnection:(AVCaptureConnection *)connection {
+     NSLog(@"Dropped a frame!");
+}
+
+@end
+
+@implementation MyCameraSession
+
+-(id)initWithCallback:(CameraSessionCallback)cb cookie:(void *)cookie {
+    if ((self = [super init]) != nil) {
+        dispatchQueue_ = dispatch_queue_create(
+                "CameraSource", DISPATCH_QUEUE_SERIAL);
+
+        session_ = [AVCaptureSession new];
+        session_.sessionPreset = AVCaptureSessionPreset1280x720;
+
+        [session_ beginConfiguration];
+
+#ifdef TARGET_IOS
+        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo
+                                 completionHandler:^(BOOL granted) {
+                                     NSLog(@"XXX granted = %d", granted);
+                                 }];
+
+        camera_ = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInDualCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront];
+
+        if (camera_ == nil) {
+            camera_ = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront];
+        }
+#else
+        camera_ = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
+#endif
+
+        NSLog(@"camera supports the following frame rates: %@",
+              camera_.activeFormat.videoSupportedFrameRateRanges);
+        
+        camera_.activeVideoMinFrameDuration = CMTimeMake(1, 30);  // limit to 30fps
+        
+        NSError *error;
+        AVCaptureDeviceInput *videoInput =
+            [AVCaptureDeviceInput deviceInputWithDevice:camera_ error:&error];
+
+        AVCaptureVideoDataOutput *videoOutput = [AVCaptureVideoDataOutput new];
+
+        // NSLog(@"available codec types: %@", videoOutput.availableVideoCodecTypes);
+        // NSLog(@"available cvpixel types: %@", videoOutput.availableVideoCVPixelFormatTypes);
+
+#if defined(TARGET_IOS)
+        videoOutput.videoSettings = nil;
+#else
+        videoOutput.videoSettings =
+            [NSDictionary dictionaryWithObjectsAndKeys:
+                AVVideoCodecTypeH264, AVVideoCodecKey,
+                nil, nil];
+#endif
+
+        delegate_ =
+            [[MyVideoOutputDelegate alloc] initWithCallback:cb cookie:cookie];
+
+        [videoOutput
+            setSampleBufferDelegate:delegate_
+                              queue:dispatchQueue_];
+
+        [session_ addInput:videoInput];
+        [session_ addOutput:videoOutput];
+
+        [session_ commitConfiguration];
+    }
+
+    return self;
+}
+
+-(void)start {
+    [session_ startRunning];
+}
+
+-(void)stop {
+    [session_ stopRunning];
+}
+
+-(void)pause {
+    [delegate_ pause];
+}
+
+-(void)resume {
+    [delegate_ resume];
+}
+
+@end
+
+void *createCameraSession(CameraSessionCallback cb, void *cookie) {
+    return (void *)(CFBridgingRetain([[MyCameraSession alloc] initWithCallback:cb cookie:cookie]));
+}
+
+void startCameraSession(void *session) {
+    [(__bridge MyCameraSession *)session start];
+}
+
+void stopCameraSession(void *session) {
+    [(__bridge MyCameraSession *)session stop];
+}
+
+void pauseCameraSession(void *session) {
+    [(__bridge MyCameraSession *)session pause];
+}
+
+void resumeCameraSession(void *session) {
+    [(__bridge MyCameraSession *)session resume];
+}
+
+void destroyCameraSession(void *session) {
+    CFBridgingRelease(session);
+}
+
+
diff --git a/host/frontend/gcastv2/libsource/DirectRenderer_iOS.cpp b/host/frontend/gcastv2/libsource/DirectRenderer_iOS.cpp
new file mode 100644
index 0000000..3c22213
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/DirectRenderer_iOS.cpp
@@ -0,0 +1,221 @@
+#include <source/DirectRenderer_iOS.h>
+
+#include <media/stagefright/avc_utils.h>
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/AMessage.h>
+
+namespace android {
+
+DirectRenderer_iOS::DirectRenderer_iOS()
+    : mVideoFormatDescription(nullptr),
+      mSession(nullptr) {
+}
+
+DirectRenderer_iOS::~DirectRenderer_iOS() {
+    if (mVideoFormatDescription) {
+        CFRelease(mVideoFormatDescription);
+        mVideoFormatDescription = NULL;
+    }
+
+    if (mSession) {
+        VTDecompressionSessionInvalidate(mSession);
+        CFRelease(mSession);
+        mSession = NULL;
+    }
+}
+
+static void OnFrameReady(
+        void *decompressionOutputRefCon,
+        void *sourceFrameRefCon,
+        OSStatus status,
+        VTDecodeInfoFlags infoFlags,
+        CVImageBufferRef imageBuffer,
+        CMTime presentationTimeStamp,
+        CMTime presentationDuration) {
+    static_cast<DirectRenderer_iOS *>(
+            decompressionOutputRefCon)->render(imageBuffer);
+}
+
+void DirectRenderer_iOS::setFormat(size_t index, const sp<AMessage> &format) {
+    ALOGI("DirectRenderer_iOS::setFormat(%zu) => %s",
+          index,
+          format->debugString().c_str());
+
+    sp<ABuffer> csd0;
+    CHECK(format->findBuffer("csd-0", &csd0));
+    CHECK(csd0->size() >= 4 && !memcmp(csd0->data(), "\x00\x00\x00\x01", 4));
+
+    sp<ABuffer> csd1;
+    CHECK(format->findBuffer("csd-1", &csd1));
+    CHECK(csd1->size() >= 4 && !memcmp(csd1->data(), "\x00\x00\x00\x01", 4));
+
+    int32_t width, height;
+    CHECK(format->findInt32("width", &width));
+    CHECK(format->findInt32("height", &height));
+
+    const uint8_t *parameterSets[2] = {
+        csd0->data() + 4,
+        csd1->data() + 4,
+    };
+
+    const size_t parameterSetSizes[2] = {
+        csd0->size() - 4,
+        csd1->size() - 4,
+    };
+
+    OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(
+            kCFAllocatorDefault,
+            sizeof(parameterSets) / sizeof(parameterSets[0]),
+            parameterSets,
+            parameterSetSizes,
+            4 /* NALUnitHeaderLength */,
+            &mVideoFormatDescription);
+
+    CHECK_EQ(status, noErr);
+
+    CFDictionaryRef videoDecoderSpecification = NULL;
+
+    CFMutableDictionaryRef destinationImageBufferAttrs =
+        CFDictionaryCreateMutable(
+                kCFAllocatorDefault,
+                0 /* capacity */,
+                &kCFTypeDictionaryKeyCallBacks,
+                &kCFTypeDictionaryValueCallBacks);
+
+    CFDictionarySetValue(
+            destinationImageBufferAttrs,
+            kCVPixelBufferOpenGLESCompatibilityKey,
+            kCFBooleanTrue);
+
+    SInt32 pixelType = kCVPixelFormatType_32BGRA;
+
+    CFNumberRef value =
+        CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &pixelType);
+
+    CFDictionarySetValue(
+            destinationImageBufferAttrs,
+            kCVPixelBufferPixelFormatTypeKey,
+            value);
+
+    CFRelease(value);
+
+    CFDictionaryRef surfaceProps =
+        CFDictionaryCreate(
+                kCFAllocatorDefault,
+                NULL /* keys */,
+                NULL /* values */,
+                0 /* numValues */,
+                &kCFTypeDictionaryKeyCallBacks,
+                &kCFTypeDictionaryValueCallBacks);
+
+    CFDictionarySetValue(
+            destinationImageBufferAttrs,
+            kCVPixelBufferIOSurfacePropertiesKey,
+            surfaceProps);
+
+    CFRelease(surfaceProps);
+    surfaceProps = NULL;
+
+    VTDecompressionOutputCallbackRecord outputCallback = {
+        .decompressionOutputCallback = OnFrameReady,
+        .decompressionOutputRefCon = this,
+    };
+
+    status = VTDecompressionSessionCreate(
+        kCFAllocatorDefault,
+        mVideoFormatDescription,
+        videoDecoderSpecification,
+        destinationImageBufferAttrs,
+        &outputCallback,
+        &mSession);
+
+    CHECK_EQ(status, noErr);
+}
+
+static sp<ABuffer> ReplaceStartCodesWithLength(const sp<ABuffer> &buffer) {
+    sp<ABuffer> outBuf = new ABuffer(buffer->size() + 128);  // Replacing 2 byte length with 4 byte startcodes takes its toll...
+    uint8_t *outData = outBuf->data();
+    size_t outOffset = 0;
+
+    const uint8_t *data = buffer->data();
+    size_t size = buffer->size();
+
+    const uint8_t *nalStart;
+    size_t nalSize;
+    while (getNextNALUnit(&data, &size, &nalStart, &nalSize, true) == OK) {
+        outData[outOffset++] = (nalSize >> 24) & 0xff;
+        outData[outOffset++] = (nalSize >> 16) & 0xff;
+        outData[outOffset++] = (nalSize >> 8) & 0xff;
+        outData[outOffset++] = nalSize & 0xff;
+        memcpy(&outData[outOffset], nalStart, nalSize);
+        outOffset += nalSize;
+    }
+
+    outBuf->setRange(0, outOffset);
+
+    return outBuf;
+}
+
+void DirectRenderer_iOS::queueAccessUnit(
+        size_t index, const sp<ABuffer> &accessUnit) {
+    sp<ABuffer> sampleBuf = ReplaceStartCodesWithLength(accessUnit);
+
+    CMBlockBufferRef blockBuffer;
+    OSStatus status = CMBlockBufferCreateWithMemoryBlock(
+            kCFAllocatorDefault,
+            sampleBuf->data(),
+            sampleBuf->size(),
+            kCFAllocatorNull,
+            NULL /* customBlockSource */,
+            0 /* offsetToData */,
+            sampleBuf->size(),
+            0 /* flags */,
+            &blockBuffer);
+
+    int64_t timeUs;
+    CHECK(accessUnit->meta()->findInt64("timeUs", &timeUs));
+
+    const CMSampleTimingInfo timing = {
+        .duration = 0,
+        .presentationTimeStamp = CMTimeMake((timeUs * 9ll) / 100ll, 90000),
+        .decodeTimeStamp = kCMTimeInvalid,
+    };
+
+    const size_t size = sampleBuf->size();
+
+    CMSampleBufferRef sampleBuffer;
+    status = CMSampleBufferCreate(
+            kCFAllocatorDefault,
+            blockBuffer,
+            true /* dataReady */,
+            NULL /* makeDataReadyCallback */,
+            0 /* makeDataReadyRefCon */,
+            mVideoFormatDescription,
+            1 /* numSamples */,
+            1 /* numSampleTimingEntries */,
+            &timing /* sampleTimingArray */,
+            1 /* numSampleSizeEntries */,
+            &size /* sampleSizeArray */,
+            &sampleBuffer);
+
+    CFRelease(blockBuffer);
+    blockBuffer = NULL;
+
+    CHECK_EQ(status, noErr);
+
+    // NSLog(@"got a buffer of size %zu\n", sampleBuf->size());
+
+    VTDecodeInfoFlags infoFlags;
+    status = VTDecompressionSessionDecodeFrame(
+            mSession,
+            sampleBuffer,
+            kVTDecodeFrame_EnableAsynchronousDecompression
+                | kVTDecodeFrame_EnableTemporalProcessing,
+            0 /* sourceFrameRefCon */,
+            &infoFlags);
+
+    CFRelease(sampleBuffer);
+    sampleBuffer = NULL;
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libsource/DisplayListsSource.cpp b/host/frontend/gcastv2/libsource/DisplayListsSource.cpp
new file mode 100644
index 0000000..e84bc09
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/DisplayListsSource.cpp
@@ -0,0 +1,38 @@
+#include <source/DisplayListsSource.h>
+
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/AMessage.h>
+
+namespace android {
+
+status_t DisplayListsSource::initCheck() const {
+    return OK;
+}
+
+sp<AMessage> DisplayListsSource::getFormat() const {
+    return nullptr;
+}
+
+status_t DisplayListsSource::start() {
+    return OK;
+}
+
+status_t DisplayListsSource::stop() {
+    return OK;
+}
+
+status_t DisplayListsSource::requestIDRFrame() {
+    return OK;
+}
+
+void DisplayListsSource::inject(const void *data, size_t size) {
+    sp<ABuffer> accessUnit = new ABuffer(size);
+    memcpy(accessUnit->data(), data, size);
+
+    accessUnit->meta()->setInt64("timeUs", ALooper::GetNowUs());
+
+    StreamingSource::onAccessUnit(accessUnit);
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/FrameBufferSource.cpp b/host/frontend/gcastv2/libsource/FrameBufferSource.cpp
new file mode 100644
index 0000000..41719c3
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/FrameBufferSource.cpp
@@ -0,0 +1,741 @@
+#include <source/FrameBufferSource.h>
+
+#include <algorithm>
+
+#include <media/stagefright/Utils.h>
+#include <media/stagefright/MediaDefs.h>
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/AMessage.h>
+#include <media/stagefright/foundation/TSPacketizer.h>
+#include <libyuv/convert.h>
+
+#include "common/vsoc/lib/screen_region_view.h"
+
+#include "host/libs/config/cuttlefish_config.h"
+
+#include "vpx/vpx_encoder.h"
+#include "vpx/vpx_codec.h"
+#include "vpx/vp8cx.h"
+
+#if ENABLE_H264
+#include "x264/x264.h"
+#endif
+
+#include <gflags/gflags.h>
+
+#define ENABLE_LOGGING          0
+
+namespace android {
+
+struct FrameBufferSource::Encoder {
+    Encoder() = default;
+    virtual ~Encoder() = default;
+
+    virtual void forceIDRFrame() = 0;
+    virtual bool isForcingIDRFrame() const = 0;
+    virtual sp<ABuffer> encode(const void *frame, int64_t timeUs) = 0;
+
+    virtual sp<AMessage> getFormat() const = 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#if ENABLE_H264
+
+struct FrameBufferSource::H264Encoder : public FrameBufferSource::Encoder {
+    H264Encoder(int width, int height, int rateHz);
+    ~H264Encoder() override;
+
+    void forceIDRFrame() override;
+    bool isForcingIDRFrame() const override;
+    sp<ABuffer> encode(const void *frame, int64_t timeUs) override;
+
+    sp<AMessage> getFormat() const override;
+
+private:
+    sp<AMessage> mFormat;
+
+    x264_param_t mParams;
+    x264_picture_t mPicIn, mPicOut;
+    x264_t *mImpl;
+
+    int mWidth, mHeight;
+    int32_t mNumFrames;
+
+    int mSizeY, mSizeUV;
+
+    std::atomic<bool> mForceIDRFrame;
+
+    void *mI420Data;
+
+#if ENABLE_LOGGING
+    sp<TSPacketizer> mPacketizer;
+    FILE *mFile;
+#endif
+};
+
+static sp<ABuffer> copy(const void *data, size_t size) {
+    sp<ABuffer> buffer = new ABuffer(size);
+    memcpy(buffer->data(), data, size);
+    return buffer;
+}
+
+FrameBufferSource::H264Encoder::H264Encoder(int width, int height, int rateHz)
+    : mImpl(nullptr),
+      mWidth(width),
+      mHeight(height),
+      mNumFrames(0),
+      mForceIDRFrame(false),
+      mI420Data(nullptr)
+#if ENABLE_LOGGING
+      ,mFile(fopen("/tmp/log.ts", "wb"))
+#endif
+{
+    CHECK((width & 1) == 0);
+    CHECK((height & 1) == 0);
+    mSizeY = width * height;
+    mSizeUV = (width / 2) * (height / 2);
+    size_t totalSize = mSizeY + 2 * mSizeUV;
+    mI420Data = malloc(totalSize);
+
+    x264_param_default_preset(&mParams, "ultrafast", "zerolatency");
+
+    mParams.i_threads = 4;
+    mParams.i_width = width;
+    mParams.i_height = height;
+    mParams.i_fps_num = rateHz;
+    mParams.i_fps_den = 1;
+    mParams.i_bitdepth = 8;
+    mParams.b_vfr_input = 0;
+    mParams.b_repeat_headers = 1;
+    mParams.b_annexb = 1;
+
+    int csp = X264_CSP_I420;
+
+    mParams.i_csp = csp;
+
+    x264_param_apply_fastfirstpass(&mParams);
+    x264_param_apply_profile(&mParams, "main");
+
+    x264_picture_alloc(
+            &mPicOut, csp, mParams.i_width, mParams.i_height);
+
+    x264_picture_init(&mPicIn);
+    mPicIn.img.i_csp = csp;
+
+    mPicIn.img.i_stride[0] = width;
+    mPicIn.img.i_stride[1] = width / 2;
+    mPicIn.img.i_stride[2] = width / 2;
+    mPicIn.img.i_plane = 3;
+
+    mImpl = x264_encoder_open(&mParams);
+    CHECK(mImpl);
+
+    x264_nal_t *headers;
+    int numNalUnits;
+    /* int size = */x264_encoder_headers(mImpl, &headers, &numNalUnits);
+
+    mFormat = new AMessage;
+    mFormat->setString("mime", MEDIA_MIMETYPE_VIDEO_AVC);
+    mFormat->setInt32("width", width);
+    mFormat->setInt32("height", height);
+
+    for (int i = 0; i < numNalUnits; ++i) {
+        sp<ABuffer> csd = copy(headers[i].p_payload, headers[i].i_payload);
+        mFormat->setBuffer(StringPrintf("csd-%d", i).c_str(), csd);
+    }
+
+    LOG(INFO) << "Format is " << mFormat->debugString().c_str();
+
+#if ENABLE_LOGGING
+    mPacketizer = new TSPacketizer;
+
+    ssize_t videoTrackIndex = mPacketizer->addTrack(mFormat);
+    LOG(INFO) << "Created video track index " << videoTrackIndex;
+#endif
+}
+
+FrameBufferSource::H264Encoder::~H264Encoder() {
+    // x264_picture_clean(&mPicOut);
+
+    x264_encoder_close(mImpl);
+    mImpl = nullptr;
+
+    free(mI420Data);
+
+#if ENABLE_LOGGING
+    if (mFile) {
+        fclose(mFile);
+        mFile = nullptr;
+    }
+#endif
+}
+
+void FrameBufferSource::H264Encoder::forceIDRFrame() {
+    mForceIDRFrame = true;
+}
+
+bool FrameBufferSource::H264Encoder::isForcingIDRFrame() const {
+    return mForceIDRFrame;
+}
+
+sp<AMessage> FrameBufferSource::H264Encoder::getFormat() const {
+    return mFormat;
+}
+
+sp<ABuffer> FrameBufferSource::H264Encoder::encode(
+        const void *frame, int64_t timeUs) {
+    if (frame) {
+        // If we don't get a new frame, we'll just repeat the previously
+        // YUV-converted frame again.
+
+        mPicIn.img.plane[0] = (uint8_t *)mI420Data;
+        mPicIn.img.plane[1] = (uint8_t *)mI420Data + mSizeY;
+        mPicIn.img.plane[2] = (uint8_t *)mI420Data + mSizeY + mSizeUV;
+
+        libyuv::ABGRToI420(
+                static_cast<const uint8_t *>(frame),
+                mWidth * 4,
+                mPicIn.img.plane[0],
+                mWidth,
+                mPicIn.img.plane[1],
+                mWidth / 2,
+                mPicIn.img.plane[2],
+                mWidth / 2,
+                mWidth,
+                mHeight);
+    }
+
+    if (mForceIDRFrame.exchange(false)) {
+        mPicIn.i_type = X264_TYPE_IDR;
+    } else {
+        mPicIn.i_type = X264_TYPE_AUTO;
+    }
+
+    x264_nal_t *nals;
+    int numNalUnits;
+
+    int size = x264_encoder_encode(
+            mImpl, &nals, &numNalUnits, &mPicIn, &mPicOut);
+
+    // LOG(INFO) << "encoded frame of size " << size;
+
+    sp<ABuffer> accessUnit = copy(nals[0].p_payload, size);
+    accessUnit->meta()->setInt64("timeUs", timeUs);
+
+#if ENABLE_LOGGING
+    sp<ABuffer> packets;
+    uint32_t flags = 0;
+
+    if (mNumFrames == 0) {
+        flags |= TSPacketizer::EMIT_PAT_AND_PMT;
+    }
+
+    if ((mNumFrames % 10) == 0) {
+        flags |= TSPacketizer::EMIT_PCR;
+    }
+
+    status_t err = mPacketizer->packetize(
+            0 /* trackIndex */,
+            accessUnit,
+            &packets,
+            flags);
+
+    CHECK(err == OK);
+
+    if (mFile) {
+        fwrite(packets->data(), 1, packets->size(), mFile);
+        fflush(mFile);
+    }
+#endif
+
+    ++mNumFrames;
+
+    return accessUnit;
+}
+
+#endif  // ENABLE_H264
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct FrameBufferSource::VPXEncoder : public FrameBufferSource::Encoder {
+    VPXEncoder(int width, int height, int rateHz);
+    ~VPXEncoder() override;
+
+    void forceIDRFrame() override;
+    bool isForcingIDRFrame() const override;
+
+    sp<ABuffer> encode(const void *frame, int64_t timeUs) override;
+
+    sp<AMessage> getFormat() const override;
+
+private:
+    int mWidth, mHeight, mRefreshRateHz;
+
+    int mSizeY, mSizeUV;
+    void *mI420Data;
+
+    vpx_codec_iface_t *mCodecInterface;
+    std::unique_ptr<vpx_codec_enc_cfg_t> mCodecConfiguration;
+
+    using contextFreeFunc = std::function<vpx_codec_err_t(vpx_codec_ctx_t *)>;
+    std::unique_ptr<vpx_codec_ctx_t, contextFreeFunc> mCodecContext;
+
+    std::atomic<bool> mForceIDRFrame;
+    bool mFirstFrame;
+    int64_t mLastTimeUs;
+
+    sp<AMessage> mFormat;
+};
+
+static int GetCPUCoreCount() {
+    int cpuCoreCount;
+
+#if defined(_SC_NPROCESSORS_ONLN)
+    cpuCoreCount = sysconf(_SC_NPROCESSORS_ONLN);
+#else
+    // _SC_NPROC_ONLN must be defined...
+    cpuCoreCount = sysconf(_SC_NPROC_ONLN);
+#endif
+
+    CHECK_GE(cpuCoreCount, 1);
+    return cpuCoreCount;
+}
+
+FrameBufferSource::VPXEncoder::VPXEncoder(int width, int height, int rateHz)
+    : mWidth(width),
+      mHeight(height),
+      mRefreshRateHz(rateHz),
+      mCodecContext(nullptr, vpx_codec_destroy),
+      mForceIDRFrame(false),
+      mFirstFrame(true),
+      mLastTimeUs(0) {
+
+    CHECK((width & 1) == 0);
+    CHECK((height & 1) == 0);
+    mSizeY = width * height;
+    mSizeUV = (width / 2) * (height / 2);
+    size_t totalSize = mSizeY + 2 * mSizeUV;
+    mI420Data = malloc(totalSize);
+
+    mCodecInterface = vpx_codec_vp8_cx();
+    mCodecConfiguration = std::make_unique<vpx_codec_enc_cfg_t>();
+
+    auto res = vpx_codec_enc_config_default(
+            mCodecInterface, mCodecConfiguration.get(), 0 /* usage */);
+
+    mCodecConfiguration->g_w = width;
+    mCodecConfiguration->g_h = height;
+    mCodecConfiguration->g_threads = std::min(GetCPUCoreCount(), 64);
+    mCodecConfiguration->g_error_resilient = false;
+    mCodecConfiguration->g_timebase.num = 1;
+    mCodecConfiguration->g_timebase.den = 1000000;
+    mCodecConfiguration->rc_target_bitrate = 2500;  // This appears to match x264
+    mCodecConfiguration->rc_end_usage = VPX_VBR;
+    mCodecConfiguration->rc_dropframe_thresh = 0;
+    mCodecConfiguration->g_lag_in_frames = 0;
+
+    mCodecConfiguration->g_profile = 0;
+
+    CHECK_EQ(res, VPX_CODEC_OK);
+
+    mCodecContext.reset(new vpx_codec_ctx_t);
+
+    res = vpx_codec_enc_init(
+            mCodecContext.get(),
+            mCodecInterface,
+            mCodecConfiguration.get(),
+            0 /* flags */);
+
+    CHECK_EQ(res, VPX_CODEC_OK);
+
+    res = vpx_codec_control(mCodecContext.get(), VP8E_SET_TOKEN_PARTITIONS, 0);
+    CHECK_EQ(res, VPX_CODEC_OK);
+
+    mFormat = new AMessage;
+    mFormat->setString("mime", MEDIA_MIMETYPE_VIDEO_VP8);
+    mFormat->setInt32("width", width);
+    mFormat->setInt32("height", height);
+}
+
+FrameBufferSource::VPXEncoder::~VPXEncoder() {
+    free(mI420Data);
+    mI420Data = nullptr;
+}
+
+void FrameBufferSource::VPXEncoder::forceIDRFrame() {
+    mForceIDRFrame = true;
+}
+
+bool FrameBufferSource::VPXEncoder::isForcingIDRFrame() const {
+    return mForceIDRFrame;
+}
+
+sp<ABuffer> FrameBufferSource::VPXEncoder::encode(
+        const void *frame, int64_t timeUs) {
+    uint8_t *yPlane = static_cast<uint8_t *>(mI420Data);
+    uint8_t *uPlane = yPlane + mSizeY;
+    uint8_t *vPlane = uPlane + mSizeUV;
+
+    libyuv::ABGRToI420(
+            static_cast<const uint8_t *>(frame),
+            mWidth * 4,
+            yPlane,
+            mWidth,
+            uPlane,
+            mWidth / 2,
+            vPlane,
+            mWidth / 2,
+            mWidth,
+            mHeight);
+
+    vpx_image_t raw_frame;
+    vpx_img_wrap(
+            &raw_frame,
+            VPX_IMG_FMT_I420,
+            mWidth,
+            mHeight,
+            2 /* stride_align */,
+            yPlane);
+
+    vpx_enc_frame_flags_t flags = 0;
+
+    if (mForceIDRFrame.exchange(false)) {
+        flags |= VPX_EFLAG_FORCE_KF;
+    }
+
+    uint32_t frameDuration;
+
+    if (!mFirstFrame) {
+        frameDuration = timeUs - mLastTimeUs;
+    } else {
+        frameDuration = 1000000 / mRefreshRateHz;
+        mFirstFrame = false;
+    }
+
+    mLastTimeUs = timeUs;
+
+    auto res = vpx_codec_encode(
+            mCodecContext.get(),
+            &raw_frame,
+            timeUs,
+            frameDuration,
+            flags,
+            VPX_DL_REALTIME);
+
+    if (res != VPX_CODEC_OK) {
+        LOG(ERROR) << "vpx_codec_encode failed w/ " << res;
+        return nullptr;
+    }
+
+    vpx_codec_iter_t iter = nullptr;
+    const vpx_codec_cx_pkt_t *packet;
+
+    sp<ABuffer> accessUnit;
+
+    while ((packet = vpx_codec_get_cx_data(mCodecContext.get(), &iter)) != nullptr) {
+        if (packet->kind == VPX_CODEC_CX_FRAME_PKT) {
+            LOG(VERBOSE)
+                << "vpx_codec_encode returned packet of size "
+                << packet->data.frame.sz;
+
+            if (accessUnit != nullptr) {
+                LOG(ERROR)
+                    << "vpx_codec_encode returned more than one packet of "
+                        "compressed data!";
+
+                return nullptr;
+            }
+
+            accessUnit = new ABuffer(packet->data.frame.sz);
+
+            memcpy(accessUnit->data(),
+                   packet->data.frame.buf,
+                   packet->data.frame.sz);
+
+            accessUnit->meta()->setInt64("timeUs", timeUs);
+        } else {
+            LOG(INFO)
+                << "vpx_codec_encode returned a packet of type "
+                << packet->kind;
+        }
+    }
+
+    return accessUnit;
+}
+
+sp<AMessage> FrameBufferSource::VPXEncoder::getFormat() const {
+    return mFormat;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+FrameBufferSource::FrameBufferSource(Format format)
+    : mInitCheck(NO_INIT),
+      mState(STOPPED),
+      mFormat(format),
+      mRegionView(nullptr),
+      mScreenWidth(0),
+      mScreenHeight(0),
+      mScreenDpi(0),
+      mScreenRate(0),
+      mOnFrameFn(nullptr) {
+    auto config = vsoc::CuttlefishConfig::Get();
+    if (config->enable_ivserver()) {
+        mRegionView = ScreenRegionView::GetInstance(vsoc::GetDomain().c_str());
+
+        mScreenWidth = mRegionView->x_res();
+        mScreenHeight = mRegionView->y_res();
+        mScreenRate = mRegionView->refresh_rate_hz();
+    }
+
+    mInitCheck = OK;
+}
+
+FrameBufferSource::~FrameBufferSource() {
+    stop();
+}
+
+status_t FrameBufferSource::initCheck() const {
+    return mInitCheck;
+}
+
+void FrameBufferSource::setParameters(const sp<AMessage> &params) {
+    std::string mime;
+    if (params != nullptr && params->findString("mime", &mime)) {
+        if (!strcasecmp(mime.c_str(), MEDIA_MIMETYPE_VIDEO_VP8)) {
+            mFormat = Format::VP8;
+#if ENABLE_H264
+        } else if (!strcasecmp(mime.c_str(), MEDIA_MIMETYPE_VIDEO_AVC)) {
+            mFormat = Format::H264;
+#endif
+        } else {
+            LOG(WARNING)
+                << "Unknown video encoding mime type requested: '"
+                << mime
+                << "', ignoring.";
+        }
+    }
+}
+
+sp<AMessage> FrameBufferSource::getFormat() const {
+    // We're not using the encoder's format (although it will be identical),
+    // because the encoder may not have been instantiated just yet.
+
+    sp<AMessage> format = new AMessage;
+    format->setString(
+            "mime",
+            mFormat == Format::H264
+                ? MEDIA_MIMETYPE_VIDEO_AVC : MEDIA_MIMETYPE_VIDEO_VP8);
+
+    format->setInt32("width", mScreenWidth);
+    format->setInt32("height", mScreenHeight);
+
+    return format;
+}
+
+status_t FrameBufferSource::start() {
+    std::lock_guard<std::mutex> autoLock(mLock);
+
+    if (mState != STOPPED) {
+        return OK;
+    }
+
+    switch (mFormat) {
+#if ENABLE_H264
+        case Format::H264:
+        {
+            mEncoder.reset(
+                    new H264Encoder(mScreenWidth, mScreenHeight, mScreenRate));
+
+            break;
+        }
+#endif
+
+        case Format::VP8:
+        {
+            mEncoder.reset(
+                    new VPXEncoder(mScreenWidth, mScreenHeight, mScreenRate));
+
+            break;
+        }
+
+        default:
+            TRESPASS();
+    }
+
+    auto config = vsoc::CuttlefishConfig::Get();
+    if (config->enable_ivserver()) {
+        mEncoder->forceIDRFrame();
+
+        mThread.reset(
+                new std::thread([this]{
+#if 0
+                    // Wait for about 1.5 frame durations to get a new frame,
+                    // then repeat the previous frame if we haven't done that
+                    // already.
+                    const int64_t kMaxFrameDelayNs =
+                        1500000000ll / mRegionView->refresh_rate_hz();
+#else
+                    // Deliver frames at about 30fps.
+                    const int64_t kMaxFrameDelayNs = 1000000000ll / 30;
+#endif
+
+                    LOG(INFO)
+                        << "RegionView claims to run at "
+                        << mRegionView->refresh_rate_hz() << " Hz.";
+
+                    bool repeatLastFrame = false;
+
+                    uint32_t prevSeqNum;
+                    while (mState != STOPPING) {
+                        struct timespec absTimeLimit;
+                        vsoc::RegionView::GetFutureTime(
+                                kMaxFrameDelayNs /* ns_from_now */, &absTimeLimit);
+
+                        int bufferIndex = mRegionView->WaitForNewFrameSince(
+                                &prevSeqNum, nullptr /* stats */, &absTimeLimit);
+
+                        const uint8_t *frame = nullptr;
+
+                        if (bufferIndex == -ETIMEDOUT) {
+                            LOG(VERBOSE) << "FrameBufferSource read timed out";
+
+                            if (repeatLastFrame) {
+                                LOG(VERBOSE) << "Repeating previous frame.";
+                            }
+                        } else if (bufferIndex >= 0) {
+                            CHECK_GE(bufferIndex, 0);
+
+                            frame = static_cast<uint8_t *>(
+                                    mRegionView->GetBuffer(bufferIndex));
+
+                            repeatLastFrame = true;
+                        } else {
+                            LOG(ERROR)
+                                << "WaitForNewFrameSince returned "
+                                << bufferIndex
+                                << " ("
+                                << strerror(-bufferIndex)
+                                << ")";
+
+                            continue;
+                        }
+
+                        if (mState == RUNNING
+                                && (frame
+                                    || repeatLastFrame
+                                    || mEncoder->isForcingIDRFrame())) {
+
+                            sp<ABuffer> accessUnit =
+                                mEncoder->encode(frame, ALooper::GetNowUs());
+
+                            if (accessUnit == nullptr) {
+                                continue;
+                            }
+
+                            StreamingSource::onAccessUnit(accessUnit);
+
+                            if (!frame) {
+                                // Only repeat the previous frame at most once.
+                                repeatLastFrame = false;
+                            }
+                        }
+                    }
+                }));
+    }
+
+    mState = RUNNING;
+
+    return OK;
+}
+
+status_t FrameBufferSource::stop() {
+    std::lock_guard<std::mutex> autoLock(mLock);
+
+    if (mState == STOPPED) {
+        return OK;
+    }
+
+    mState = STOPPING;
+
+    if (mThread) {
+        mThread->join();
+        mThread.reset();
+    }
+
+    mState = STOPPED;
+
+    mEncoder.reset();
+
+    return OK;
+}
+
+status_t FrameBufferSource::pause() {
+    std::lock_guard<std::mutex> autoLock(mLock);
+
+    if (mState == PAUSED) {
+        return OK;
+    }
+
+    if (mState != RUNNING) {
+        return INVALID_OPERATION;
+    }
+
+    mState = PAUSED;
+
+    LOG(VERBOSE) << "Now paused.";
+
+    return OK;
+}
+
+status_t FrameBufferSource::resume() {
+    std::lock_guard<std::mutex> autoLock(mLock);
+
+    if (mState == RUNNING) {
+        return OK;
+    }
+
+    if (mState != PAUSED) {
+        return INVALID_OPERATION;
+    }
+
+    mState = RUNNING;
+
+    LOG(VERBOSE) << "Now running.";
+
+    return OK;
+}
+
+bool FrameBufferSource::paused() const {
+    return mState == PAUSED;
+}
+
+status_t FrameBufferSource::requestIDRFrame() {
+    mEncoder->forceIDRFrame();
+
+    return OK;
+}
+
+void FrameBufferSource::setScreenParams(const int32_t screenParams[4]) {
+    mScreenWidth = screenParams[0];
+    mScreenHeight = screenParams[1];
+    mScreenDpi = screenParams[2];
+    mScreenRate = screenParams[3];
+}
+
+void FrameBufferSource::injectFrame(const void *data, size_t size) {
+    // Only used in the case of CrosVM operation.
+    (void)size;
+
+    std::lock_guard<std::mutex> autoLock(mLock);
+    if (/* noone is listening || */ mState != State::RUNNING) {
+        return;
+    }
+
+    sp<ABuffer> accessUnit = mEncoder->encode(data, ALooper::GetNowUs());
+
+    StreamingSource::onAccessUnit(accessUnit);
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libsource/HostToGuestComms.cpp b/host/frontend/gcastv2/libsource/HostToGuestComms.cpp
new file mode 100644
index 0000000..0cf9b72
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/HostToGuestComms.cpp
@@ -0,0 +1,379 @@
+#ifdef TARGET_ANDROID_DEVICE_NO_JAVA
+#include "HostToGuestComms.h"
+#else
+#include <source/HostToGuestComms.h>
+#endif
+
+#include <https/SafeCallbackable.h>
+#include <https/Support.h>
+
+#ifdef TARGET_ANDROID_DEVICE_NO_JAVA
+#include <android-base/logging.h>
+#else
+#include <media/stagefright/foundation/ADebug.h>
+#endif
+
+HostToGuestComms::HostToGuestComms(
+        std::shared_ptr<RunLoop> runLoop,
+        bool isServer,
+        uint32_t cid,
+        uint16_t port,
+        ReceiveCb onReceive)
+    : mRunLoop(runLoop),
+      mIsServer(isServer),
+      mOnReceive(onReceive),
+      mServerSock(-1),
+      mSock(-1),
+      mInBufferLen(0),
+      mSendPending(false),
+      mConnected(false) {
+    int s = socket(AF_VSOCK, SOCK_STREAM, 0);
+    CHECK_GE(s, 0);
+
+    LOG(INFO) << "HostToGuestComms created socket " << s;
+
+    makeFdNonblocking(s);
+
+    sockaddr_vm addr;
+    memset(&addr, 0, sizeof(addr));
+    addr.svm_family = AF_VSOCK;
+    addr.svm_port = port;
+    addr.svm_cid = cid;
+
+    int res;
+    if (mIsServer) {
+        LOG(INFO)
+            << "Binding to cid "
+            << (addr.svm_cid == VMADDR_CID_ANY)
+                    ? "VMADDR_CID_ANY" : std::to_string(addr.svm_cid);
+
+        res = bind(s, reinterpret_cast<const sockaddr *>(&addr), sizeof(addr));
+
+        if (res) {
+            LOG(ERROR)
+                << (mIsServer ? "bind" : "connect")
+                << " FAILED w/ errno "
+                << errno
+                << " ("
+                << strerror(errno)
+                << ")";
+        }
+
+        CHECK(!res);
+
+        res = listen(s, 4);
+        CHECK(!res);
+
+        mServerSock = s;
+    } else {
+        mSock = s;
+        mConnectToAddr = addr;
+    }
+}
+
+HostToGuestComms::~HostToGuestComms() {
+    if (mSock >= 0) {
+        mRunLoop->cancelSocket(mSock);
+
+        close(mSock);
+        mSock = -1;
+    }
+
+    if (mServerSock >= 0) {
+        mRunLoop->cancelSocket(mServerSock);
+
+        close(mServerSock);
+        mServerSock = -1;
+    }
+}
+
+void HostToGuestComms::start() {
+    if (mIsServer) {
+        mRunLoop->postSocketRecv(
+                mServerSock,
+                makeSafeCallback(this, &HostToGuestComms::onServerConnection));
+    } else {
+        mRunLoop->postWithDelay(
+                std::chrono::milliseconds(5000), 
+                makeSafeCallback(
+                    this,
+                    &HostToGuestComms::onAttemptToConnect,
+                    mConnectToAddr));
+    }
+}
+
+void HostToGuestComms::send(const void *data, size_t size, bool addFraming) {
+    if (!size) {
+        return;
+    }
+
+    std::lock_guard autoLock(mLock);
+
+    size_t offset = mOutBuffer.size();
+
+    if (addFraming) {
+        uint32_t packetLen = size;
+        size_t totalSize = sizeof(packetLen) + size;
+
+        mOutBuffer.resize(offset + totalSize);
+        memcpy(mOutBuffer.data() + offset, &packetLen, sizeof(packetLen));
+        memcpy(mOutBuffer.data() + offset + sizeof(packetLen), data, size);
+    } else {
+        mOutBuffer.resize(offset + size);
+        memcpy(mOutBuffer.data() + offset, data, size);
+    }
+
+    if (mSock >= 0 && (mIsServer || mConnected) && !mSendPending) {
+        mSendPending = true;
+        mRunLoop->postSocketSend(
+                mSock,
+                makeSafeCallback(this, &HostToGuestComms::onSocketSend));
+    }
+}
+
+void HostToGuestComms::onServerConnection() {
+    int s = accept(mServerSock, nullptr, nullptr);
+
+    if (s >= 0) {
+        if (mSock >= 0) {
+            LOG(INFO) << "Rejecting client, we already have one.";
+
+            // We already have a client.
+            close(s);
+            s = -1;
+        } else {
+            LOG(INFO) << "Accepted client socket " << s << ".";
+
+            makeFdNonblocking(s);
+
+            mSock = s;
+            mRunLoop->postSocketRecv(
+                    mSock,
+                    makeSafeCallback(this, &HostToGuestComms::onSocketReceive));
+
+            std::lock_guard autoLock(mLock);
+            if (!mOutBuffer.empty()) {
+                CHECK(!mSendPending);
+
+                mSendPending = true;
+                mRunLoop->postSocketSend(
+                        mSock,
+                        makeSafeCallback(
+                            this, &HostToGuestComms::onSocketSend));
+            }
+        }
+    }
+
+    mRunLoop->postSocketRecv(
+            mServerSock,
+            makeSafeCallback(this, &HostToGuestComms::onServerConnection));
+}
+
+void HostToGuestComms::onSocketReceive() {
+    ssize_t n;
+    for (;;) {
+        static constexpr size_t kChunkSize = 65536;
+
+        mInBuffer.resize(mInBufferLen + kChunkSize);
+
+        do {
+            n = recv(mSock, mInBuffer.data() + mInBufferLen, kChunkSize, 0);
+        } while (n < 0 && errno == EINTR);
+
+        if (n <= 0) {
+            break;
+        }
+
+        mInBufferLen += static_cast<size_t>(n);
+    }
+
+    int savedErrno = errno;
+
+    drainInBuffer();
+
+    if ((n < 0 && savedErrno != EAGAIN && savedErrno != EWOULDBLOCK)
+            || n == 0) {
+        LOG(ERROR) << "Client is gone.";
+
+        // Client is gone.
+        mRunLoop->cancelSocket(mSock);
+
+        mSendPending = false;
+
+        close(mSock);
+        mSock = -1;
+        return;
+    }
+
+    mRunLoop->postSocketRecv(
+            mSock,
+            makeSafeCallback(this, &HostToGuestComms::onSocketReceive));
+}
+
+void HostToGuestComms::drainInBuffer() {
+    for (;;) {
+        uint32_t packetLen;
+
+        if (mInBufferLen < sizeof(packetLen)) {
+            return;
+        }
+
+        memcpy(&packetLen, mInBuffer.data(), sizeof(packetLen));
+
+        size_t totalLen = sizeof(packetLen) + packetLen;
+
+        if (mInBufferLen < totalLen) {
+            return;
+        }
+
+        if (mOnReceive) {
+            // LOG(INFO) << "Dispatching packet of size " << packetLen;
+
+            mOnReceive(mInBuffer.data() + sizeof(packetLen), packetLen);
+        }
+
+        mInBuffer.erase(mInBuffer.begin(), mInBuffer.begin() + totalLen);
+        mInBufferLen -= totalLen;
+    }
+}
+
+void HostToGuestComms::onSocketSend() {
+    std::lock_guard autoLock(mLock);
+
+    CHECK(mSendPending);
+    mSendPending = false;
+
+    if (mSock < 0) {
+        return;
+    }
+
+    ssize_t n;
+    while (!mOutBuffer.empty()) {
+        do {
+            n = ::send(mSock, mOutBuffer.data(), mOutBuffer.size(), 0);
+        } while (n < 0 && errno == EINTR);
+
+        if (n <= 0) {
+            break;
+        }
+
+        mOutBuffer.erase(mOutBuffer.begin(), mOutBuffer.begin() + n);
+    }
+
+    if ((n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) || n == 0) {
+        LOG(ERROR) << "Client is gone.";
+
+        // Client is gone.
+        mRunLoop->cancelSocket(mSock);
+
+        close(mSock);
+        mSock = -1;
+        return;
+    }
+
+    if (!mOutBuffer.empty()) {
+        mSendPending = true;
+        mRunLoop->postSocketSend(
+                mSock,
+                makeSafeCallback(this, &HostToGuestComms::onSocketSend));
+    }
+}
+
+void HostToGuestComms::onAttemptToConnect(const sockaddr_vm &addr) {
+    LOG(VERBOSE) << "Attempting to connect to cid " << addr.svm_cid;
+
+    int res;
+    do {
+        res = connect(
+            mSock, reinterpret_cast<const sockaddr *>(&addr), sizeof(addr));
+    } while (res < 0 && errno == EINTR);
+
+    if (res < 0) {
+        if (errno == EINPROGRESS) {
+            LOG(VERBOSE) << "EINPROGRESS, waiting to check the connection.";
+
+            mRunLoop->postSocketSend(
+                    mSock,
+                    makeSafeCallback(
+                        this, &HostToGuestComms::onCheckConnection, addr));
+
+            return;
+        }
+
+        LOG(INFO)
+            << "Our attempt to connect to the guest FAILED w/ error "
+            << errno
+            << " ("
+            << strerror(errno)
+            << "), will try again shortly.";
+
+        mRunLoop->postWithDelay(
+                std::chrono::milliseconds(5000),
+                makeSafeCallback(
+                    this, &HostToGuestComms::onAttemptToConnect, addr));
+
+        return;
+    }
+
+    onConnected();
+}
+
+void HostToGuestComms::onCheckConnection(const sockaddr_vm &addr) {
+    int err;
+
+    int res;
+    do {
+        socklen_t errSize = sizeof(err);
+
+        res = getsockopt(mSock, SOL_SOCKET, SO_ERROR, &err, &errSize);
+    } while (res < 0 && errno == EINTR);
+
+    CHECK(!res);
+
+    if (!err) {
+        onConnected();
+    } else {
+        LOG(VERBOSE)
+            << "Connection failed w/ error "
+            << err
+            << " ("
+            << strerror(err)
+            << "), will try again shortly.";
+
+        // Is there a better way of cancelling the (failed) connection that
+        // somehow is still in progress on the socket and restarting it?
+        mRunLoop->cancelSocket(mSock);
+
+        close(mSock);
+        mSock = socket(AF_VSOCK, SOCK_STREAM, 0);
+        CHECK_GE(mSock, 0);
+
+        makeFdNonblocking(mSock);
+
+        mRunLoop->postWithDelay(
+                std::chrono::milliseconds(5000),
+                makeSafeCallback(
+                    this, &HostToGuestComms::onAttemptToConnect, addr));
+    }
+}
+
+void HostToGuestComms::onConnected() {
+    LOG(INFO) << "Connected to guest.";
+
+    std::lock_guard autoLock(mLock);
+
+    mConnected = true;
+    CHECK(!mSendPending);
+
+    if (!mOutBuffer.empty()) {
+        mSendPending = true;
+        mRunLoop->postSocketSend(
+                mSock,
+                makeSafeCallback(this, &HostToGuestComms::onSocketSend));
+    }
+
+    mRunLoop->postSocketRecv(
+            mSock,
+            makeSafeCallback(this, &HostToGuestComms::onSocketReceive));
+}
+
diff --git a/host/frontend/gcastv2/libsource/StreamingSource.cpp b/host/frontend/gcastv2/libsource/StreamingSource.cpp
new file mode 100644
index 0000000..47e0e4d
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/StreamingSource.cpp
@@ -0,0 +1,34 @@
+#include <source/StreamingSource.h>
+
+#include <media/stagefright/foundation/ADebug.h>
+
+namespace android {
+
+StreamingSource::StreamingSource()
+    : mNotify(nullptr),
+      mCallbackFn(nullptr) {
+}
+
+void StreamingSource::setNotify(const sp<AMessage> &notify) {
+    CHECK(!mCallbackFn);
+    CHECK(notify);
+    mNotify = notify;
+}
+
+void StreamingSource::setCallback(std::function<void(const sp<ABuffer> &)> cb) {
+    CHECK(!mNotify);
+    CHECK(cb);
+    mCallbackFn = cb;
+}
+
+void StreamingSource::onAccessUnit(const sp<ABuffer> &accessUnit) {
+    if (mCallbackFn) {
+        mCallbackFn(accessUnit);
+    } else {
+        sp<AMessage> notify = mNotify->dup();
+        notify->setBuffer("accessUnit", accessUnit);
+        notify->post();
+    }
+}
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libsource/TouchSource.cpp b/host/frontend/gcastv2/libsource/TouchSource.cpp
new file mode 100644
index 0000000..55d57ac
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/TouchSource.cpp
@@ -0,0 +1,41 @@
+#include <source/TouchSource.h>
+
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/AMessage.h>
+
+namespace android {
+
+status_t TouchSource::initCheck() const {
+    return OK;
+}
+
+sp<AMessage> TouchSource::getFormat() const {
+    return nullptr;
+}
+
+status_t TouchSource::start() {
+    return OK;
+}
+
+status_t TouchSource::stop() {
+    return OK;
+}
+
+status_t TouchSource::requestIDRFrame() {
+    return OK;
+}
+
+void TouchSource::inject(bool down, int32_t x, int32_t y) {
+    sp<ABuffer> accessUnit = new ABuffer(3 * sizeof(int32_t));
+    int32_t *data = reinterpret_cast<int32_t *>(accessUnit->data());
+    data[0] = down;
+    data[1] = x;
+    data[2] = y;
+
+    accessUnit->meta()->setInt64("timeUs", ALooper::GetNowUs());
+
+    StreamingSource::onAccessUnit(accessUnit);
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/VSOCTouchSink.cpp b/host/frontend/gcastv2/libsource/VSOCTouchSink.cpp
new file mode 100644
index 0000000..9161150
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/VSOCTouchSink.cpp
@@ -0,0 +1,61 @@
+#include <source/VSOCTouchSink.h>
+
+#include <media/stagefright/foundation/ABuffer.h>
+
+#include "host/libs/config/cuttlefish_config.h"
+
+namespace android {
+
+VSOCTouchSink::VSOCTouchSink()
+    : mInputEventsRegionView(
+            InputEventsRegionView::GetInstance(vsoc::GetDomain().c_str())) {
+}
+
+void VSOCTouchSink::onAccessUnit(const sp<ABuffer> &accessUnit) {
+    const int32_t *data =
+        reinterpret_cast<const int32_t *>(accessUnit->data());
+
+    if (accessUnit->size() == 3 * sizeof(int32_t)) {
+        // Legacy: Single Touch Emulation.
+
+        bool down = data[0] != 0;
+        int x = data[1];
+        int y = data[2];
+
+        LOG(VERBOSE)
+            << "Received touch (down="
+            << down
+            << ", x="
+            << x
+            << ", y="
+            << y;
+
+        mInputEventsRegionView->HandleSingleTouchEvent(down, x, y);
+        return;
+    }
+
+    CHECK_EQ(accessUnit->size(), 5 * sizeof(int32_t));
+
+    int id = data[0];
+    bool initialDown = data[1] != 0;
+    int x = data[2];
+    int y = data[3];
+    int slot = data[4];
+
+    LOG(VERBOSE)
+        << "Received touch (id="
+        << id
+        << ", initialDown="
+        << initialDown
+        << ", x="
+        << x
+        << ", y="
+        << y
+        << ", slot="
+        << slot;
+
+    mInputEventsRegionView->HandleMultiTouchEvent(id, initialDown, x, y, slot);
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/VideoSink.cpp b/host/frontend/gcastv2/libsource/VideoSink.cpp
new file mode 100644
index 0000000..108b869
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/VideoSink.cpp
@@ -0,0 +1,43 @@
+#include <source/VideoSink.h>
+
+#include <media/stagefright/avc_utils.h>
+#include <media/stagefright/Utils.h>
+#include <media/stagefright/MetaData.h>
+
+namespace android {
+
+VideoSink::VideoSink()
+    : mRenderer(new DirectRenderer_iOS),
+      mFirstAccessUnit(true) {
+}
+
+void VideoSink::onMessageReceived(const sp<AMessage> &msg) {
+    switch (msg->what()) {
+        case kWhatAccessUnit:
+        {
+            sp<ABuffer> accessUnit;
+            CHECK(msg->findBuffer("accessUnit", &accessUnit));
+
+            if (mFirstAccessUnit) {
+                mFirstAccessUnit = false;
+
+                sp<MetaData> meta = MakeAVCCodecSpecificData(accessUnit);
+                CHECK(meta != nullptr);
+
+                sp<AMessage> format;
+                CHECK_EQ(OK, convertMetaDataToMessage(meta, &format));
+
+                mRenderer->setFormat(0, format);
+            }
+
+            mRenderer->queueAccessUnit(0, accessUnit);
+            break;
+        }
+
+        default:
+            TRESPASS();
+    }
+}
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/include/source/AACPlayer.h b/host/frontend/gcastv2/libsource/include/source/AACPlayer.h
new file mode 100644
index 0000000..208717c
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/AACPlayer.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include "AudioQueueBufferManager.h"
+#include "BufferQueue.h"
+
+#include <utils/Errors.h>
+
+#include <sys/types.h>
+
+#include <AudioToolbox/AudioToolbox.h>
+
+#include <memory>
+
+namespace android {
+
+#define USE_AUDIO_UNIT          1
+
+struct AACPlayer {
+    explicit AACPlayer();
+    ~AACPlayer();
+
+    AACPlayer(const AACPlayer &) = delete;
+    AACPlayer &operator=(const AACPlayer &) = delete;
+
+    status_t feedADTSFrame(const void *frame, size_t size);
+
+    int32_t sampleRateHz() const;
+
+private:
+    AudioStreamBasicDescription mInFormat, mOutFormat;
+    AudioConverterRef mConverter;
+
+#if USE_AUDIO_UNIT
+    AUGraph mGraph;
+    AUNode mOutputNode;
+    std::unique_ptr<BufferQueue> mBufferQueue;
+#else
+    AudioQueueRef mQueue;
+    std::unique_ptr<AudioQueueBufferManager> mBufferManager;
+#endif
+
+    int32_t mSampleRateHz;
+    int64_t mNumFramesSubmitted;
+
+    status_t init(int32_t sampleRate, size_t channelCount);
+
+#if !USE_AUDIO_UNIT
+    static void PlayCallback(
+            void *_cookie, AudioQueueRef queue, AudioQueueBufferRef buffer);
+#else
+    static OSStatus FeedInput(
+            void *cookie,
+            AudioUnitRenderActionFlags *flags,
+            const AudioTimeStamp *timeStamp,
+            UInt32 bus,
+            UInt32 numFrames,
+            AudioBufferList *data);
+#endif
+};
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libsource/include/source/AudioQueueBufferManager.h b/host/frontend/gcastv2/libsource/include/source/AudioQueueBufferManager.h
new file mode 100644
index 0000000..0e4336c
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/AudioQueueBufferManager.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <utils/Errors.h>
+
+#include <AudioToolbox/AudioToolbox.h>
+
+#include <deque>
+#include <mutex>
+
+namespace android {
+
+struct AudioQueueBufferManager {
+    AudioQueueBufferManager(AudioQueueRef queue, size_t count, size_t size);
+    ~AudioQueueBufferManager();
+
+    status_t initCheck() const;
+
+    size_t bufferSize() const;
+
+    AudioQueueBufferManager(const AudioQueueBufferManager &) = delete;
+    AudioQueueBufferManager &operator=(const AudioQueueBufferManager &) = delete;
+
+    AudioQueueBufferRef acquire(int64_t timeoutUs = -1ll);
+    void release(AudioQueueBufferRef buffer);
+
+private:
+    status_t mInitCheck;
+    AudioQueueRef mQueue;
+    size_t mBufferSize;
+
+    std::mutex mLock;
+    std::condition_variable mCondition;
+
+    std::deque<AudioQueueBufferRef> mBuffers;
+};
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/include/source/AudioSink.h b/host/frontend/gcastv2/libsource/include/source/AudioSink.h
new file mode 100644
index 0000000..ea46e2e
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/AudioSink.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <source/StreamingSink.h>
+
+#include "AACPlayer.h"
+
+#define LOG_AUDIO       0
+
+namespace android {
+
+struct AudioSink : public StreamingSink {
+    AudioSink();
+
+    void onMessageReceived(const sp<AMessage> &msg) override;
+
+private:
+    std::unique_ptr<AACPlayer> mAACPlayer;
+
+#if LOG_AUDIO
+    FILE *mFile;
+#endif
+};
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/include/source/AudioSource.h b/host/frontend/gcastv2/libsource/include/source/AudioSource.h
new file mode 100644
index 0000000..08c5c4d
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/AudioSource.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+#pragma once
+
+#include <media/stagefright/foundation/ABuffer.h>
+#include <source/StreamingSource.h>
+
+#include <memory>
+#include <thread>
+
+#define SIMULATE_AUDIO          0
+
+namespace vsoc {
+    class RegionWorker;
+    namespace audio_data {
+        class AudioDataRegionView;
+    }
+}
+
+namespace android {
+
+struct AudioSource : public StreamingSource {
+    using AudioDataRegionView = vsoc::audio_data::AudioDataRegionView;
+
+    enum class Format {
+        AAC,
+        OPUS,
+        G711_ALAW,
+        G711_ULAW,
+    };
+    // ADTS framing is only supported for Format::AAC.
+    explicit AudioSource(Format format, bool useADTSFraming = false);
+
+    AudioSource(const AudioSource &) = delete;
+    AudioSource &operator=(const AudioSource &) = delete;
+
+    ~AudioSource() override;
+
+    status_t initCheck() const override;
+
+    sp<AMessage> getFormat() const override;
+
+    status_t start() override;
+    status_t stop() override;
+
+    status_t requestIDRFrame() override;
+
+    void inject(const void *data, size_t size);
+
+private:
+    enum State {
+        STOPPING,
+        STOPPED,
+        RUNNING,
+        PAUSED
+    };
+
+    struct Encoder;
+    struct AACEncoder;
+    struct OPUSEncoder;
+    struct G711Encoder;
+
+    status_t mInitCheck;
+    State mState;
+    AudioDataRegionView *mRegionView;
+    std::unique_ptr<vsoc::RegionWorker> mRegionWorker;
+    std::unique_ptr<Encoder> mEncoder;
+
+    sp<AMessage> mFormat;
+
+    std::mutex mLock;
+    std::unique_ptr<std::thread> mThread;
+
+#if SIMULATE_AUDIO
+    static constexpr int32_t kSampleRate = 44100;
+    static constexpr int32_t kNumChannels = 2;
+    static constexpr size_t kNumFramesPerBuffer = 400;
+    static constexpr int32_t kFrequency = 500;
+    size_t mPhase;
+#endif
+};
+
+}  // namespace android
+
+
diff --git a/host/frontend/gcastv2/libsource/include/source/BufferQueue.h b/host/frontend/gcastv2/libsource/include/source/BufferQueue.h
new file mode 100644
index 0000000..ed11c5b
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/BufferQueue.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <utils/Errors.h>
+
+#include <deque>
+#include <mutex>
+
+namespace android {
+
+struct BufferQueue {
+    BufferQueue(size_t count, size_t size);
+    ~BufferQueue();
+
+    status_t initCheck() const;
+
+    size_t bufferSize() const;
+
+    BufferQueue(const BufferQueue &) = delete;
+    BufferQueue &operator=(const BufferQueue &) = delete;
+
+    void *acquire(int64_t timeoutUs = -1ll);
+    void queue(void *buffer);
+
+    void *dequeueBegin(size_t *size);
+    void dequeueEnd(size_t size);
+
+private:
+    struct Buffer {
+        void *mData;
+        size_t mOffset;
+    };
+
+    status_t mInitCheck;
+    size_t mBufferSize;
+
+    std::mutex mLock;
+    std::condition_variable mCondition;
+
+    std::deque<void *> mEmptyBuffers;
+    std::deque<Buffer> mFullBuffers;
+};
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/include/source/CVMTouchSink.h b/host/frontend/gcastv2/libsource/include/source/CVMTouchSink.h
new file mode 100644
index 0000000..a66c30f
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/CVMTouchSink.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <https/RunLoop.h>
+#include <source/StreamingSink.h>
+
+#include <memory>
+#include <vector>
+
+#include <linux/input.h>
+#include <linux/uinput.h>
+
+namespace android {
+
+struct CVMTouchSink
+    : public StreamingSink, public std::enable_shared_from_this<CVMTouchSink> {
+    explicit CVMTouchSink(std::shared_ptr<RunLoop> runLoop, int serverFd);
+    ~CVMTouchSink() override;
+
+    void start();
+
+    void onAccessUnit(const sp<ABuffer> &accessUnit) override;
+
+private:
+    std::shared_ptr<RunLoop> mRunLoop;
+    int mServerFd;
+
+    int mClientFd;
+
+    std::mutex mLock;
+    std::vector<uint8_t> mOutBuffer;
+    bool mSendPending;
+
+    void onServerConnection();
+    void onSocketSend();
+
+    void sendEvents(const std::vector<input_event> &events);
+};
+
+}  // namespace android
+
+
diff --git a/host/frontend/gcastv2/libsource/include/source/CameraSource.h b/host/frontend/gcastv2/libsource/include/source/CameraSource.h
new file mode 100644
index 0000000..e4934df
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/CameraSource.h
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+#pragma once
+
+#include <source/StreamingSource.h>
+
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/AMessage.h>
+
+#include <vector>
+#include <memory>
+#include <thread>
+
+namespace android {
+
+struct CameraSource : public StreamingSource {
+    CameraSource();
+
+    CameraSource(const CameraSource &) = delete;
+    CameraSource &operator=(const CameraSource &) = delete;
+
+    ~CameraSource() override;
+
+    status_t initCheck() const override;
+
+    sp<AMessage> getFormat() const override;
+
+    status_t start() override;
+    status_t stop() override;
+
+    status_t pause() override;
+    status_t resume() override;
+
+    bool paused() const override;
+
+    status_t requestIDRFrame() override;
+
+private:
+    enum State {
+        STOPPING,
+        STOPPED,
+        RUNNING,
+        PAUSED
+    };
+
+    status_t mInitCheck;
+    State mState;
+
+    std::vector<sp<ABuffer>> mCSD;
+
+    void *mSession;
+
+    std::mutex mLock;
+
+    static void onFrameData(
+            void *cookie,
+            ssize_t csdIndex,
+            int64_t timeUs,
+            const void *data,
+            size_t size);
+
+    void onFrameData(
+            ssize_t csdIndex, int64_t timeUs, const void *data, size_t size);
+
+    sp<ABuffer> prependCSD(const sp<ABuffer> &accessUnit) const;
+};
+
+}  // namespace android
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void (*CameraSessionCallback)(
+        void *cookie, ssize_t csdIndex, int64_t timeUs, const void *data, size_t size);
+
+void *createCameraSession(CameraSessionCallback cb, void *cookie);
+void startCameraSession(void *session);
+void stopCameraSession(void *session);
+void pauseCameraSession(void *session);
+void resumeCameraSession(void *session);
+void destroyCameraSession(void *session);
+
+#ifdef __cplusplus
+}
+#endif
+
+
+
diff --git a/host/frontend/gcastv2/libsource/include/source/DirectRenderer_iOS.h b/host/frontend/gcastv2/libsource/include/source/DirectRenderer_iOS.h
new file mode 100644
index 0000000..8cb1fb1
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/DirectRenderer_iOS.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <VideoToolbox/VideoToolbox.h>
+#include <media/stagefright/foundation/AMessage.h>
+
+namespace android {
+
+struct DirectRenderer_iOS {
+    DirectRenderer_iOS();
+    ~DirectRenderer_iOS();
+
+    void setFormat(size_t index, const sp<AMessage> &format);
+    void queueAccessUnit(size_t index, const sp<ABuffer> &accessUnit);
+
+    void render(CVImageBufferRef imageBuffer);
+
+private:
+    CMVideoFormatDescriptionRef mVideoFormatDescription;
+    VTDecompressionSessionRef mSession;
+
+    DISALLOW_EVIL_CONSTRUCTORS(DirectRenderer_iOS);
+};
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/include/source/DisplayListsSource.h b/host/frontend/gcastv2/libsource/include/source/DisplayListsSource.h
new file mode 100644
index 0000000..a51b3d0
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/DisplayListsSource.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <source/StreamingSource.h>
+
+#include <media/stagefright/foundation/ABuffer.h>
+
+namespace android {
+
+struct DisplayListsSource : public StreamingSource {
+    DisplayListsSource() = default;
+
+    status_t initCheck() const override;
+    sp<AMessage> getFormat() const override;
+    status_t start() override;
+    status_t stop() override;
+    status_t requestIDRFrame() override;
+
+    void inject(const void *data, size_t size);
+};
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libsource/include/source/FrameBufferSource.h b/host/frontend/gcastv2/libsource/include/source/FrameBufferSource.h
new file mode 100644
index 0000000..1e7447b
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/FrameBufferSource.h
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+#pragma once
+
+#include <source/StreamingSource.h>
+
+#include <media/stagefright/foundation/AMessage.h>
+
+#include <functional>
+#include <memory>
+#include <thread>
+
+#define ENABLE_H264     0
+
+namespace vsoc {
+    namespace screen {
+        class ScreenRegionView;
+    }
+}
+
+namespace android {
+
+struct ABuffer;
+
+struct FrameBufferSource : public StreamingSource {
+    using ScreenRegionView = vsoc::screen::ScreenRegionView;
+
+    enum class Format {
+        H264,
+        VP8,
+    };
+
+    explicit FrameBufferSource(Format format);
+
+    FrameBufferSource(const FrameBufferSource &) = delete;
+    FrameBufferSource &operator=(const FrameBufferSource &) = delete;
+
+    ~FrameBufferSource() override;
+
+    status_t initCheck() const override;
+
+    void setParameters(const sp<AMessage> &params) override;
+
+    sp<AMessage> getFormat() const override;
+
+    status_t start() override;
+    status_t stop() override;
+
+    status_t pause() override;
+    status_t resume() override;
+
+    bool paused() const override;
+
+    status_t requestIDRFrame() override;
+
+    void setScreenParams(const int32_t screenParams[4]);
+    void injectFrame(const void *data, size_t size);
+
+private:
+    enum State {
+        STOPPING,
+        STOPPED,
+        RUNNING,
+        PAUSED
+    };
+
+    struct Encoder;
+#if ENABLE_H264
+    struct H264Encoder;
+#endif
+    struct VPXEncoder;
+
+    status_t mInitCheck;
+    State mState;
+    Format mFormat;
+    ScreenRegionView *mRegionView;
+    std::unique_ptr<Encoder> mEncoder;
+
+    std::mutex mLock;
+    std::unique_ptr<std::thread> mThread;
+
+    int32_t mScreenWidth, mScreenHeight, mScreenDpi, mScreenRate;
+
+    std::function<void(const sp<ABuffer> &)> mOnFrameFn;
+};
+
+}  // namespace android
+
+
diff --git a/host/frontend/gcastv2/libsource/include/source/HostToGuestComms.h b/host/frontend/gcastv2/libsource/include/source/HostToGuestComms.h
new file mode 100644
index 0000000..6458a86
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/HostToGuestComms.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <https/RunLoop.h>
+
+#include <sys/socket.h>
+#include "common/libs/fs/vm_sockets.h"
+
+#include <memory>
+
+struct HostToGuestComms : std::enable_shared_from_this<HostToGuestComms> {
+    using ReceiveCb = std::function<void(const void *data, size_t size)>;
+
+    // Used to communicate with the guest userspace "RemoterService".
+    static constexpr uint16_t kPortMain = 8555;
+
+    // Used to carry updated framebuffers from guest to host.
+    static constexpr uint16_t kPortVideo = 5580;
+
+    // Used to carry audio data from guest to host.
+    static constexpr uint16_t kPortAudio = 8556;
+
+    explicit HostToGuestComms(
+            std::shared_ptr<RunLoop> runLoop,
+            bool isServer,
+            uint32_t cid,
+            uint16_t port,
+            ReceiveCb onReceive);
+
+    ~HostToGuestComms();
+
+    void start();
+
+    void send(const void *data, size_t size, bool addFraming = true);
+
+private:
+    std::shared_ptr<RunLoop> mRunLoop;
+    bool mIsServer;
+    ReceiveCb mOnReceive;
+
+    int mServerSock;
+    int mSock;
+    sockaddr_vm mConnectToAddr;
+
+    std::vector<uint8_t> mInBuffer;
+    size_t mInBufferLen;
+
+    std::mutex mLock;
+    std::vector<uint8_t> mOutBuffer;
+    bool mSendPending;
+
+    bool mConnected;
+
+    void onServerConnection();
+    void onSocketReceive();
+    void onSocketSend();
+    void drainInBuffer();
+
+    void onAttemptToConnect(const sockaddr_vm &addr);
+    void onCheckConnection(const sockaddr_vm &addr);
+    void onConnected();
+};
+
diff --git a/host/frontend/gcastv2/libsource/include/source/StreamingSink.h b/host/frontend/gcastv2/libsource/include/source/StreamingSink.h
new file mode 100644
index 0000000..ed3c62b
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/StreamingSink.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <media/stagefright/foundation/ABuffer.h>
+
+namespace android {
+
+struct StreamingSink {
+    explicit StreamingSink() = default;
+    virtual ~StreamingSink() = default;
+
+    virtual void onAccessUnit(const sp<ABuffer> &accessUnit) = 0;
+};
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/include/source/StreamingSource.h b/host/frontend/gcastv2/libsource/include/source/StreamingSource.h
new file mode 100644
index 0000000..94878d1
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/StreamingSource.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+#pragma once
+
+#include <utils/Errors.h>
+#include <utils/RefBase.h>
+
+#include <media/stagefright/foundation/AMessage.h>
+
+namespace android {
+
+struct StreamingSource {
+    explicit StreamingSource();
+
+    StreamingSource(const StreamingSource &) = delete;
+    StreamingSource &operator=(const StreamingSource &) = delete;
+
+    virtual ~StreamingSource() = default;
+
+    virtual status_t initCheck() const = 0;
+
+    void setNotify(const sp<AMessage> &notify);
+    void setCallback(std::function<void(const sp<ABuffer> &)> cb);
+
+    virtual void setParameters(const sp<AMessage> &params) {
+        (void)params;
+    }
+
+    virtual sp<AMessage> getFormat() const = 0;
+
+    virtual status_t start() = 0;
+    virtual status_t stop() = 0;
+
+    virtual bool canPause() { return false; }
+    virtual status_t pause() { return -EINVAL; }
+    virtual status_t resume() { return -EINVAL; }
+
+    virtual bool paused() const { return false; }
+
+    virtual status_t requestIDRFrame() = 0;
+
+protected:
+    void onAccessUnit(const sp<ABuffer> &accessUnit);
+
+private:
+    sp<AMessage> mNotify;
+    std::function<void(const sp<ABuffer> &)> mCallbackFn;
+};
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/include/source/TouchSource.h b/host/frontend/gcastv2/libsource/include/source/TouchSource.h
new file mode 100644
index 0000000..3478bad
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/TouchSource.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <source/StreamingSource.h>
+
+namespace android {
+
+struct TouchSource : public StreamingSource {
+    TouchSource() = default;
+
+    status_t initCheck() const override;
+    sp<AMessage> getFormat() const override;
+    status_t start() override;
+    status_t stop() override;
+    status_t requestIDRFrame() override;
+
+    void inject(bool down, int32_t x, int32_t y);
+};
+
+}  // namespace android
diff --git a/host/frontend/gcastv2/libsource/include/source/VSOCTouchSink.h b/host/frontend/gcastv2/libsource/include/source/VSOCTouchSink.h
new file mode 100644
index 0000000..8281478
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/VSOCTouchSink.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <source/StreamingSink.h>
+
+#include "common/vsoc/lib/input_events_region_view.h"
+
+namespace android {
+
+struct VSOCTouchSink : public StreamingSink {
+    VSOCTouchSink();
+
+    void onAccessUnit(const sp<ABuffer> &accessUnit) override;
+
+private:
+    using InputEventsRegionView = vsoc::input_events::InputEventsRegionView;
+
+    InputEventsRegionView *mInputEventsRegionView;
+};
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/libsource/include/source/VideoSink.h b/host/frontend/gcastv2/libsource/include/source/VideoSink.h
new file mode 100644
index 0000000..ad516e6
--- /dev/null
+++ b/host/frontend/gcastv2/libsource/include/source/VideoSink.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <source/StreamingSink.h>
+
+#include <source/DirectRenderer_iOS.h>
+
+namespace android {
+
+struct VideoSink : public StreamingSink {
+    VideoSink();
+
+    void onMessageReceived(const sp<AMessage> &msg) override;
+
+private:
+    std::unique_ptr<DirectRenderer_iOS> mRenderer;
+    bool mFirstAccessUnit;
+};
+
+}  // namespace android
+
diff --git a/host/frontend/gcastv2/webrtc/AdbWebSocketHandler.cpp b/host/frontend/gcastv2/webrtc/AdbWebSocketHandler.cpp
new file mode 100644
index 0000000..8d77a83
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/AdbWebSocketHandler.cpp
@@ -0,0 +1,244 @@
+#include <webrtc/AdbWebSocketHandler.h>
+
+#include "Utils.h"
+
+#include <https/BaseConnection.h>
+#include <https/Support.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/hexdump.h>
+#include <media/stagefright/Utils.h>
+
+#include <unistd.h>
+
+using namespace android;
+
+struct AdbWebSocketHandler::AdbConnection : public BaseConnection {
+    explicit AdbConnection(
+            AdbWebSocketHandler *parent,
+            std::shared_ptr<RunLoop> runLoop,
+            int sock);
+
+    void send(const void *_data, size_t size);
+
+protected:
+    ssize_t processClientRequest(const void *data, size_t size) override;
+    void onDisconnect(int err) override;
+
+private:
+    AdbWebSocketHandler *mParent;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+AdbWebSocketHandler::AdbConnection::AdbConnection(
+        AdbWebSocketHandler *parent,
+        std::shared_ptr<RunLoop> runLoop,
+        int sock)
+    : BaseConnection(runLoop, sock),
+      mParent(parent) {
+}
+
+// Thanks for calling it a crc32, adb documentation!
+static uint32_t computeNotACrc32(const void *_data, size_t size) {
+    auto data = static_cast<const uint8_t *>(_data);
+    uint32_t sum = 0;
+    for (size_t i = 0; i < size; ++i) {
+        sum += data[i];
+    }
+
+    return sum;
+}
+
+static int verifyAdbHeader(
+        const void *_data, size_t size, size_t *_payloadLength) {
+    auto data = static_cast<const uint8_t *>(_data);
+
+    *_payloadLength = 0;
+
+    if (size < 24) {
+        return -EAGAIN;
+    }
+
+    uint32_t command = U32LE_AT(data);
+    uint32_t magic = U32LE_AT(data + 20);
+
+    if (command != (magic ^ 0xffffffff)) {
+        return -EINVAL;
+    }
+
+    uint32_t payloadLength = U32LE_AT(data + 12);
+
+    if (size < 24 + payloadLength) {
+        return -EAGAIN;
+    }
+
+    auto payloadCrc = U32LE_AT(data + 16);
+    auto crc32 = computeNotACrc32(data + 24, payloadLength);
+
+    if (payloadCrc != crc32) {
+        return -EINVAL;
+    }
+
+    *_payloadLength = payloadLength;
+
+    return 0;
+}
+
+ssize_t AdbWebSocketHandler::AdbConnection::processClientRequest(
+        const void *_data, size_t size) {
+    auto data = static_cast<const uint8_t *>(_data);
+
+    LOG(VERBOSE)
+        << "AdbConnection::processClientRequest (size = " << size << ")";
+
+    // android::hexdump(data, size);
+
+    size_t payloadLength;
+    int err = verifyAdbHeader(data, size, &payloadLength);
+
+    if (err) {
+        return err;
+    }
+
+    mParent->sendMessage(
+            data, payloadLength + 24, WebSocketHandler::SendMode::binary);
+
+    return payloadLength + 24;
+}
+
+void AdbWebSocketHandler::AdbConnection::onDisconnect(int err) {
+    LOG(INFO) << "AdbConnection::onDisconnect(err=" << err << ")";
+
+    mParent->sendMessage(
+            nullptr /* data */,
+            0 /* size */,
+            WebSocketHandler::SendMode::closeConnection);
+}
+
+void AdbWebSocketHandler::AdbConnection::send(const void *_data, size_t size) {
+    BaseConnection::send(_data, size);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+AdbWebSocketHandler::AdbWebSocketHandler(
+        std::shared_ptr<RunLoop> runLoop,
+        const std::string &adb_host_and_port)
+    : mRunLoop(runLoop),
+      mSocket(-1) {
+    LOG(INFO) << "Connecting to " << adb_host_and_port;
+
+    auto err = setupSocket(adb_host_and_port);
+    CHECK(!err);
+
+    mAdbConnection = std::make_shared<AdbConnection>(this, mRunLoop, mSocket);
+}
+
+AdbWebSocketHandler::~AdbWebSocketHandler() {
+    if (mSocket >= 0) {
+        close(mSocket);
+        mSocket = -1;
+    }
+}
+
+void AdbWebSocketHandler::run() {
+    mAdbConnection->run();
+}
+
+int AdbWebSocketHandler::setupSocket(const std::string &adb_host_and_port) {
+    auto colonPos = adb_host_and_port.find(":");
+    if (colonPos == std::string::npos) {
+        return -EINVAL;
+    }
+
+    auto host = adb_host_and_port.substr(0, colonPos);
+
+    const char *portString = adb_host_and_port.c_str() + colonPos + 1;
+    char *end;
+    unsigned long port = strtoul(portString, &end, 10);
+
+    if (end == portString || *end != '\0' || port > 65535) {
+        return -EINVAL;
+    }
+
+    int err;
+
+    int sock = socket(PF_INET, SOCK_STREAM, 0);
+
+    if (sock < 0) {
+        err = -errno;
+        goto bail;
+    }
+
+    makeFdNonblocking(sock);
+
+    sockaddr_in addr;
+    memset(addr.sin_zero, 0, sizeof(addr.sin_zero));
+    addr.sin_family = AF_INET;
+    addr.sin_addr.s_addr = inet_addr(host.c_str());
+    addr.sin_port = htons(port);
+
+    if (connect(sock,
+                reinterpret_cast<const sockaddr *>(&addr),
+                sizeof(addr)) < 0
+            && errno != EINPROGRESS) {
+        err = -errno;
+        goto bail2;
+    }
+
+    mSocket = sock;
+
+    return 0;
+
+bail2:
+    close(sock);
+    sock = -1;
+
+bail:
+    return err;
+}
+
+int AdbWebSocketHandler::handleMessage(
+        uint8_t headerByte, const uint8_t *msg, size_t len) {
+    LOG(VERBOSE)
+        << "headerByte = "
+        << android::StringPrintf("0x%02x", (unsigned)headerByte);
+
+    // android::hexdump(msg, len);
+
+    if (!(headerByte & 0x80)) {
+        // I only want to receive whole messages here, not fragments.
+        return -EINVAL;
+    }
+
+    auto opcode = headerByte & 0x1f;
+    switch (opcode) {
+        case 0x8:
+        {
+            // closeConnection.
+            break;
+        }
+
+        case 0x2:
+        {
+            // binary
+
+            size_t payloadLength;
+            int err = verifyAdbHeader(msg, len, &payloadLength);
+
+            if (err || len != 24 + payloadLength) {
+                LOG(ERROR) << "websocket message is not a valid adb message.";
+                return -EINVAL;
+            }
+
+            mAdbConnection->send(msg, len);
+            break;
+        }
+
+        default:
+            return -EINVAL;
+    }
+
+    return 0;
+}
+
diff --git a/host/frontend/gcastv2/webrtc/Android.bp b/host/frontend/gcastv2/webrtc/Android.bp
new file mode 100644
index 0000000..85a7568
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/Android.bp
@@ -0,0 +1,98 @@
+cc_library_static {
+    name: "libwebrtc",
+    host_supported: true,
+    srcs: [
+        "AdbWebSocketHandler.cpp",
+        "DTLS.cpp",
+        "G711Packetizer.cpp",
+// "H264Packetizer.cpp",
+        "MyWebSocketHandler.cpp",
+        "OpusPacketizer.cpp",
+        "Packetizer.cpp",
+        "RTPSender.cpp",
+        "RTPSession.cpp",
+        "RTPSocketHandler.cpp",
+        "SCTPHandler.cpp",
+        "SDP.cpp",
+        "ServerState.cpp",
+        "STUNMessage.cpp",
+        "Utils.cpp",
+        "VP8Packetizer.cpp",
+    ],
+    target: {
+        host: {
+            cflags: [
+                "-DTARGET_ANDROID",
+            ],
+            static_libs: [
+                "libcuttlefish_host_config",
+                "libgflags",
+                "libjsoncpp",
+                "libsource",
+            ],
+            shared_libs: [
+                "libbase",
+            ],
+            header_libs: [
+                "cuttlefish_common_headers",
+                "cuttlefish_kernel_headers",
+            ],
+        },
+        android: {
+            enabled: false,
+        },
+    },
+    arch: {
+        x86: {
+            enabled: false,
+        }
+    },
+    static_libs: [
+        "libandroidglue",
+        "libhttps",
+        "libsrtp2",
+    ],
+    shared_libs: [
+        "libssl",
+    ],
+    local_include_dirs: ["include"],
+    export_include_dirs: ["include"],
+}
+
+cc_binary_host {
+    name: "webRTC",
+    srcs: [
+        "webRTC.cpp",
+    ],
+    header_libs: [
+        "cuttlefish_glog",
+    ],
+    shared_libs: [
+        "libbase",
+        "libcrypto",
+        "libcuttlefish_utils",
+        "libFraunhoferAAC",
+        "libopus",
+        "libssl",
+        "libvpx",
+// "libx264",
+        "libyuv",
+    ],
+    static_libs: [
+        "libandroidglue",
+        "libcuttlefish_host_config",
+        "libgflags",
+        "libhttps",
+        "libjsoncpp",
+        "libsource",
+        "libsrtp2",
+        "libwebrtc",
+    ],
+    cpp_std: "experimental",
+    cflags: [
+        "-DTARGET_ANDROID",
+    ],
+    defaults: ["cuttlefish_host_only"],
+}
+
+
diff --git a/host/frontend/gcastv2/webrtc/DTLS.cpp b/host/frontend/gcastv2/webrtc/DTLS.cpp
new file mode 100644
index 0000000..f61a59d
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/DTLS.cpp
@@ -0,0 +1,455 @@
+#include <webrtc/DTLS.h>
+
+#include <webrtc/RTPSocketHandler.h>
+
+#include <https/SafeCallbackable.h>
+#include <https/SSLSocket.h>
+#include <https/Support.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <utils/KeyStore.h>
+
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <sstream>
+
+#if defined(TARGET_ANDROID_DEVICE) && defined(TARGET_ANDROID)
+#error Only one of TARGET_ANDROID or TARGET_ANDROID_DEVICE may be specified
+#endif
+
+static int gDTLSInstanceIndex;
+
+// static
+void DTLS::Init() {
+    SSL_library_init();
+    SSL_load_error_strings();
+    OpenSSL_add_ssl_algorithms();
+
+    auto err = srtp_init();
+    CHECK_EQ(err, srtp_err_status_ok);
+
+    gDTLSInstanceIndex = SSL_get_ex_new_index(
+            0, const_cast<char *>("DTLSInstance index"), NULL, NULL, NULL);
+
+}
+
+bool DTLS::useCertificate(std::shared_ptr<X509> cert) {
+    // I'm assuming that ownership of the certificate is transferred, so I'm
+    // adding an extra reference...
+    CHECK_EQ(1, X509_up_ref(cert.get()));
+
+    return cert != nullptr && 1 == SSL_CTX_use_certificate(mCtx, cert.get());
+}
+
+bool DTLS::usePrivateKey(std::shared_ptr<EVP_PKEY> key) {
+    // I'm assuming that ownership of the key in SSL_CTX_use_PrivateKey is
+    // transferred, so I'm adding an extra reference...
+    CHECK_EQ(1, EVP_PKEY_up_ref(key.get()));
+
+    return key != nullptr
+        && 1 == SSL_CTX_use_PrivateKey(mCtx, key.get())
+        && 1 == SSL_CTX_check_private_key(mCtx);
+}
+
+DTLS::DTLS(
+        std::shared_ptr<RTPSocketHandler> handler,
+        DTLS::Mode mode,
+        std::shared_ptr<X509> cert,
+        std::shared_ptr<EVP_PKEY> key,
+        const std::string &remoteFingerprint,
+        bool useSRTP)
+    : mState(State::UNINITIALIZED),
+      mHandler(handler),
+      mMode(mode),
+      mRemoteFingerprint(remoteFingerprint),
+      mUseSRTP(useSRTP),
+      mCtx(nullptr),
+      mSSL(nullptr),
+      mBioR(nullptr),
+      mBioW(nullptr),
+      mSRTPInbound(nullptr),
+      mSRTPOutbound(nullptr) {
+    mCtx = SSL_CTX_new(DTLSv1_2_method());
+    CHECK(mCtx);
+
+    int result = SSL_CTX_set_cipher_list(
+            mCtx, "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH");
+
+    CHECK_EQ(result, 1);
+
+    SSL_CTX_set_verify(
+            mCtx,
+            SSL_VERIFY_PEER
+                | SSL_VERIFY_CLIENT_ONCE
+                | SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
+            &DTLS::OnVerifyPeerCertificate);
+
+    CHECK(useCertificate(cert));
+    CHECK(usePrivateKey(key));
+
+    if (mUseSRTP) {
+        result = SSL_CTX_set_tlsext_use_srtp(mCtx, "SRTP_AES128_CM_SHA1_80");
+        CHECK_EQ(result, 0);
+    }
+
+    mSSL = SSL_new(mCtx);
+    CHECK(mSSL);
+
+    SSL_set_ex_data(mSSL, gDTLSInstanceIndex, this);
+
+    mBioR = BIO_new(BIO_s_mem());
+    CHECK(mBioR);
+
+    mBioW = BIO_new(BIO_s_mem());
+    CHECK(mBioW);
+
+    SSL_set_bio(mSSL, mBioR, mBioW);
+
+    if (mode == Mode::CONNECT) {
+        SSL_set_connect_state(mSSL);
+    } else {
+        SSL_set_accept_state(mSSL);
+    }
+}
+
+DTLS::~DTLS() {
+    if (mSRTPOutbound) {
+        srtp_dealloc(mSRTPOutbound);
+        mSRTPOutbound = nullptr;
+    }
+
+    if (mSRTPInbound) {
+        srtp_dealloc(mSRTPInbound);
+        mSRTPInbound = nullptr;
+    }
+
+    if (mSSL) {
+        SSL_shutdown(mSSL);
+    }
+
+    SSL_free(mSSL);
+    mSSL = nullptr;
+
+    mBioW = mBioR = nullptr;
+
+    SSL_CTX_free(mCtx);
+    mCtx = nullptr;
+}
+
+// static
+int DTLS::OnVerifyPeerCertificate(int /* ok */, X509_STORE_CTX *ctx) {
+    LOG(VERBOSE) << "OnVerifyPeerCertificate";
+
+    SSL *ssl = static_cast<SSL *>(X509_STORE_CTX_get_ex_data(
+            ctx, SSL_get_ex_data_X509_STORE_CTX_idx()));
+
+    DTLS *me = static_cast<DTLS *>(SSL_get_ex_data(ssl, gDTLSInstanceIndex));
+
+    std::unique_ptr<X509, std::function<void(X509 *)>> cert(
+            SSL_get_peer_certificate(ssl), X509_free);
+
+    if (!cert) {
+        LOG(ERROR) << "SSLSocket::isPeerCertificateValid no certificate.";
+
+        return 0;
+    }
+
+    auto spacePos = me->mRemoteFingerprint.find(" ");
+    CHECK(spacePos != std::string::npos);
+    auto digestName = me->mRemoteFingerprint.substr(0, spacePos);
+    CHECK(!strcasecmp(digestName.c_str(), "sha-256"));
+
+    const EVP_MD *digest = EVP_get_digestbyname("sha256");
+
+    unsigned char md[EVP_MAX_MD_SIZE];
+    unsigned int n;
+    int res = X509_digest(cert.get(), digest, md, &n);
+    CHECK_EQ(res, 1);
+
+    std::stringstream ss;
+    for (unsigned int i = 0; i < n; ++i) {
+        if (i > 0) {
+            ss << ":";
+        }
+
+        auto byte = md[i];
+
+        auto nibble = byte >> 4;
+        ss << (char)((nibble < 10) ? ('0' + nibble) : ('A' + nibble - 10));
+
+        nibble = byte & 0x0f;
+        ss << (char)((nibble < 10) ? ('0' + nibble) : ('A' + nibble - 10));
+    }
+
+    LOG(VERBOSE)
+        << "Client offered certificate w/ fingerprint "
+        << ss.str();
+
+    LOG(VERBOSE) << "should be: " << me->mRemoteFingerprint;
+
+    auto remoteFingerprintHash = me->mRemoteFingerprint.substr(spacePos + 1);
+    bool match = !strcasecmp(remoteFingerprintHash.c_str(), ss.str().c_str());
+
+    if (!match) {
+        LOG(ERROR)
+            << "The peer's certificate's fingerprint does not match that "
+            << "published in the SDP!";
+    }
+
+    return match;
+}
+
+void DTLS::connect(const sockaddr_storage &remoteAddr) {
+    CHECK_EQ(static_cast<int>(mState), static_cast<int>(State::UNINITIALIZED));
+
+    mRemoteAddr = remoteAddr;
+    mState = State::CONNECTING;
+
+    tryConnecting();
+}
+
+void DTLS::doTheThing(int res) {
+    LOG(VERBOSE) << "doTheThing(" << res << ")";
+
+    int err = SSL_get_error(mSSL, res);
+
+    switch (err) {
+        case SSL_ERROR_WANT_READ:
+        {
+            LOG(VERBOSE) << "SSL_ERROR_WANT_READ";
+
+            queueOutputDataFromDTLS();
+            break;
+        }
+
+        case SSL_ERROR_WANT_WRITE:
+        {
+            LOG(VERBOSE) << "SSL_ERROR_WANT_WRITE";
+            break;
+        }
+
+        case SSL_ERROR_NONE:
+        {
+            LOG(VERBOSE) << "SSL_ERROR_NONE";
+            break;
+        }
+
+        case SSL_ERROR_SYSCALL:
+        default:
+        {
+            LOG(ERROR)
+                << "DTLS stack returned error "
+                << err
+                << " ("
+                << SSL_state_string_long(mSSL)
+#if !defined(TARGET_ANDROID) && !defined(TARGET_ANDROID_DEVICE)
+                << ", "
+                << SSL_rstate_string_long(mSSL)
+#endif
+                << ")";
+        }
+    }
+}
+
+void DTLS::queueOutputDataFromDTLS() {
+    auto handler = mHandler.lock();
+
+    if (!handler) {
+        return;
+    }
+
+    int n;
+
+    do {
+        char buf[RTPSocketHandler::kMaxUDPPayloadSize];
+        n = BIO_read(mBioW, buf, sizeof(buf));
+
+        if (n > 0) {
+            LOG(VERBOSE) << "queueing " << n << " bytes of output data from DTLS.";
+
+            handler->queueDatagram(
+                    mRemoteAddr, buf, static_cast<size_t>(n));
+        } else if (BIO_should_retry(mBioW)) {
+            continue;
+        } else {
+            CHECK(!"Should not be here");
+        }
+    } while (n > 0);
+}
+
+void DTLS::tryConnecting() {
+    CHECK_EQ(static_cast<int>(mState), static_cast<int>(State::CONNECTING));
+
+    int res =
+        (mMode == Mode::CONNECT)
+            ? SSL_connect(mSSL) : SSL_accept(mSSL);
+
+    if (res != 1) {
+        doTheThing(res);
+    } else {
+        queueOutputDataFromDTLS();
+
+        LOG(INFO) << "DTLS connection established.";
+        mState = State::CONNECTED;
+
+        auto handler = mHandler.lock();
+        if (handler) {
+            if (mUseSRTP) {
+                getKeyingMaterial();
+            }
+
+            handler->notifyDTLSConnected();
+        }
+    }
+}
+
+void DTLS::inject(const uint8_t *data, size_t size) {
+    LOG(VERBOSE) << "injecting " << size << " bytes into DTLS stack.";
+
+    auto n = BIO_write(mBioR, data, size);
+    CHECK_EQ(n, static_cast<int>(size));
+
+    if (mState == State::CONNECTING) {
+        if (!SSL_is_init_finished(mSSL)) {
+            tryConnecting();
+        }
+    }
+}
+
+void DTLS::getKeyingMaterial() {
+    static constexpr char kLabel[] = "EXTRACTOR-dtls_srtp";
+
+    // These correspond to the chosen option SRTP_AES128_CM_SHA1_80, passed
+    // to SSL_CTX_set_tlsext_use_srtp before. c/f RFC 5764 4.1.2
+
+    uint8_t material[(SRTP_AES_128_KEY_LEN + SRTP_SALT_LEN) * 2];
+
+    auto res = SSL_export_keying_material(
+            mSSL,
+            material,
+            sizeof(material),
+            kLabel,
+            strlen(kLabel),
+            nullptr /* context */,
+            0 /* contextlen */,
+            0 /* use_context */);
+
+    CHECK_EQ(res, 1);
+
+    // LOG(INFO) << "keying material:";
+    // hexdump(material, sizeof(material));
+
+    size_t offset = 0;
+    const uint8_t *clientKey = &material[offset];
+    offset += SRTP_AES_128_KEY_LEN;
+    const uint8_t *serverKey = &material[offset];
+    offset += SRTP_AES_128_KEY_LEN;
+    const uint8_t *clientSalt = &material[offset];
+    offset += SRTP_SALT_LEN;
+    const uint8_t *serverSalt = &material[offset];
+    offset += SRTP_SALT_LEN;
+
+    CHECK_EQ(offset, sizeof(material));
+
+    std::string sendKey(
+            reinterpret_cast<const char *>(clientKey), SRTP_AES_128_KEY_LEN);
+
+    sendKey.append(
+            reinterpret_cast<const char *>(clientSalt), SRTP_SALT_LEN);
+
+    std::string receiveKey(
+            reinterpret_cast<const char *>(serverKey), SRTP_AES_128_KEY_LEN);
+
+    receiveKey.append(
+            reinterpret_cast<const char *>(serverSalt), SRTP_SALT_LEN);
+
+    if (mMode == Mode::CONNECT) {
+        CreateSRTPSession(&mSRTPInbound, receiveKey, ssrc_any_inbound);
+        CreateSRTPSession(&mSRTPOutbound, sendKey, ssrc_any_outbound);
+    } else {
+        CreateSRTPSession(&mSRTPInbound, sendKey, ssrc_any_inbound);
+        CreateSRTPSession(&mSRTPOutbound, receiveKey, ssrc_any_outbound);
+    }
+}
+
+// static
+void DTLS::CreateSRTPSession(
+        srtp_t *session,
+        const std::string &keyAndSalt,
+        srtp_ssrc_type_t direction) {
+    srtp_policy_t policy;
+    memset(&policy, 0, sizeof(policy));
+
+    srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&policy.rtp);
+    srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&policy.rtcp);
+
+    policy.ssrc.type = direction;
+    policy.ssrc.value = 0;
+
+    policy.key =
+        const_cast<unsigned char *>(
+                reinterpret_cast<const unsigned char *>(keyAndSalt.c_str()));
+
+    policy.allow_repeat_tx = 1;
+    policy.next = nullptr;
+
+    auto ret = srtp_create(session, &policy);
+    CHECK_EQ(ret, srtp_err_status_ok);
+}
+
+size_t DTLS::protect(void *data, size_t size, bool isRTP) {
+    int len = static_cast<int>(size);
+
+    auto ret =
+        isRTP
+            ? srtp_protect(mSRTPOutbound, data, &len)
+            : srtp_protect_rtcp(mSRTPOutbound, data, &len);
+
+    CHECK_EQ(ret, srtp_err_status_ok);
+
+    return static_cast<size_t>(len);
+}
+
+size_t DTLS::unprotect(void *data, size_t size, bool isRTP) {
+    int len = static_cast<int>(size);
+
+    auto ret =
+        isRTP
+            ? srtp_unprotect(mSRTPInbound, data, &len)
+            : srtp_unprotect_rtcp(mSRTPInbound, data, &len);
+
+    if (ret == srtp_err_status_replay_fail) {
+        LOG(WARNING)
+            << "srtp_unprotect"
+            << (isRTP ? "" : "_rtcp")
+            << " returned srtp_err_status_replay_fail, ignoring packet.";
+
+        return 0;
+    }
+
+    CHECK_EQ(ret, srtp_err_status_ok);
+
+    return static_cast<size_t>(len);
+}
+
+ssize_t DTLS::readApplicationData(void *data, size_t size) {
+    auto res = SSL_read(mSSL, data, size);
+
+    if (res < 0) {
+        doTheThing(res);
+        return -1;
+    }
+
+    return res;
+}
+
+ssize_t DTLS::writeApplicationData(const void *data, size_t size) {
+    auto res = SSL_write(mSSL, data, size);
+
+    queueOutputDataFromDTLS();
+
+    // May have to queue the data and "doTheThing" on failure...
+    CHECK_EQ(res, static_cast<int>(size));
+
+    return res;
+}
diff --git a/host/frontend/gcastv2/webrtc/G711Packetizer.cpp b/host/frontend/gcastv2/webrtc/G711Packetizer.cpp
new file mode 100644
index 0000000..718aeb0
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/G711Packetizer.cpp
@@ -0,0 +1,126 @@
+#include <webrtc/G711Packetizer.h>
+
+#include "Utils.h"
+
+#include <webrtc/RTPSocketHandler.h>
+
+#include <https/SafeCallbackable.h>
+#include <media/stagefright/foundation/AMessage.h>
+#include <media/stagefright/Utils.h>
+
+using namespace android;
+
+G711Packetizer::G711Packetizer(
+        Mode mode,
+        std::shared_ptr<RunLoop> runLoop,
+        std::shared_ptr<StreamingSource> audioSource)
+    : mMode(mode),
+      mRunLoop(runLoop),
+      mAudioSource(audioSource),
+      mNumSamplesRead(0),
+      mStartTimeMedia(0),
+      mFirstInTalkspurt(true) {
+}
+
+void G711Packetizer::run() {
+    auto weak_this = std::weak_ptr<G711Packetizer>(shared_from_this());
+
+    mAudioSource->setCallback(
+            [weak_this](const sp<ABuffer> &accessUnit) {
+                auto me = weak_this.lock();
+                if (me) {
+                    me->mRunLoop->post(
+                            makeSafeCallback(
+                                me.get(), &G711Packetizer::onFrame, accessUnit));
+                }
+            });
+
+    mAudioSource->start();
+}
+
+void G711Packetizer::onFrame(const sp<ABuffer> &accessUnit) {
+    int64_t timeUs;
+    CHECK(accessUnit->meta()->findInt64("timeUs", &timeUs));
+
+    auto now = std::chrono::steady_clock::now();
+
+    if (mNumSamplesRead == 0) {
+        mStartTimeMedia = timeUs;
+        mStartTimeReal = now;
+    }
+
+    ++mNumSamplesRead;
+
+    LOG(VERBOSE)
+        << "got accessUnit of size "
+        << accessUnit->size()
+        << " at time "
+        << timeUs;
+
+    packetize(accessUnit, timeUs);
+}
+
+void G711Packetizer::packetize(const sp<ABuffer> &accessUnit, int64_t timeUs) {
+    LOG(VERBOSE) << "Received G711 frame of size " << accessUnit->size();
+
+    const uint8_t PT = (mMode == Mode::ALAW) ? 8 : 0;
+    static constexpr uint32_t SSRC = 0x8badf00d;
+
+    // XXX Retransmission packets add 2 bytes (for the original seqNum), should
+    // probably reserve that amount in the original packets so we don't exceed
+    // the MTU on retransmission.
+    static const size_t kMaxSRTPPayloadSize =
+        RTPSocketHandler::kMaxUDPPayloadSize - SRTP_MAX_TRAILER_LEN;
+
+    const uint8_t *audioData = accessUnit->data();
+    size_t size = accessUnit->size();
+
+    uint32_t rtpTime = ((timeUs - mStartTimeMedia) * 8) / 1000;
+
+#if 0
+    static uint32_t lastRtpTime = 0;
+    LOG(INFO) << "rtpTime = " << rtpTime << " [+" << (rtpTime - lastRtpTime) << "]";
+    lastRtpTime = rtpTime;
+#endif
+
+    CHECK_LE(12 + size, kMaxSRTPPayloadSize);
+
+    std::vector<uint8_t> packet(12 + size);
+    uint8_t *data = packet.data();
+
+    packet[0] = 0x80;
+    packet[1] = PT;
+
+    if (mFirstInTalkspurt) {
+        packet[1] |= 0x80;  // (M)ark
+        mFirstInTalkspurt = false;
+    }
+
+    SET_U16(&data[2], 0);  // seqNum
+    SET_U32(&data[4], rtpTime);
+    SET_U32(&data[8], SSRC);
+
+    memcpy(&data[12], audioData, size);
+
+    queueRTPDatagram(&packet);
+}
+
+uint32_t G711Packetizer::rtpNow() const {
+    if (mNumSamplesRead == 0) {
+        return 0;
+    }
+
+    auto now = std::chrono::steady_clock::now();
+    auto timeSinceStart = now - mStartTimeReal;
+
+    auto us_since_start =
+        std::chrono::duration_cast<std::chrono::microseconds>(
+                timeSinceStart).count();
+
+    return (us_since_start * 8) / 1000;
+}
+
+android::status_t G711Packetizer::requestIDRFrame() {
+    return mAudioSource->requestIDRFrame();
+}
+
diff --git a/host/frontend/gcastv2/webrtc/H264Packetizer.cpp b/host/frontend/gcastv2/webrtc/H264Packetizer.cpp
new file mode 100644
index 0000000..bd79fbe
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/H264Packetizer.cpp
@@ -0,0 +1,268 @@
+#include <webrtc/H264Packetizer.h>
+
+#include "Utils.h"
+
+#include <webrtc/RTPSocketHandler.h>
+
+#include <https/SafeCallbackable.h>
+#include <media/stagefright/foundation/AMessage.h>
+#include <media/stagefright/MediaDefs.h>
+#include <media/stagefright/avc_utils.h>
+#include <media/stagefright/Utils.h>
+
+using namespace android;
+
+H264Packetizer::H264Packetizer(
+        std::shared_ptr<RunLoop> runLoop,
+        std::shared_ptr<FrameBufferSource> frameBufferSource)
+    : mRunLoop(runLoop),
+      mFrameBufferSource(frameBufferSource),
+      mNumSamplesRead(0),
+      mStartTimeMedia(0) {
+}
+
+void H264Packetizer::run() {
+    auto weak_this = std::weak_ptr<H264Packetizer>(shared_from_this());
+
+    mFrameBufferSource->setCallback(
+            [weak_this](const sp<ABuffer> &accessUnit) {
+                auto me = weak_this.lock();
+                if (me) {
+                    me->mRunLoop->post(
+                            makeSafeCallback(
+                                me.get(), &H264Packetizer::onFrame, accessUnit));
+                }
+            });
+
+    mFrameBufferSource->start();
+}
+
+void H264Packetizer::onFrame(const sp<ABuffer> &accessUnit) {
+    int64_t timeUs;
+    CHECK(accessUnit->meta()->findInt64("timeUs", &timeUs));
+
+    auto now = std::chrono::steady_clock::now();
+
+    if (mNumSamplesRead == 0) {
+        mStartTimeMedia = timeUs;
+        mStartTimeReal = now;
+    }
+
+    ++mNumSamplesRead;
+
+    LOG(VERBOSE)
+        << "got accessUnit of size "
+        << accessUnit->size()
+        << " at time "
+        << timeUs;
+
+    packetize(accessUnit, timeUs);
+}
+
+void H264Packetizer::packetize(const sp<ABuffer> &accessUnit, int64_t timeUs) {
+    static constexpr uint8_t PT = 96;
+    static constexpr uint32_t SSRC = 0xdeadbeef;
+    static constexpr uint8_t STAP_A = 24;
+    static constexpr uint8_t FU_A = 28;
+
+    // XXX Retransmission packets add 2 bytes (for the original seqNum), should
+    // probably reserve that amount in the original packets so we don't exceed
+    // the MTU on retransmission.
+    static const size_t kMaxSRTPPayloadSize =
+        RTPSocketHandler::kMaxUDPPayloadSize - SRTP_MAX_TRAILER_LEN;
+
+    const uint8_t *data = accessUnit->data();
+    size_t size = accessUnit->size();
+
+    uint32_t rtpTime = ((timeUs - mStartTimeMedia) * 9) / 100;
+
+    std::vector<std::pair<size_t, size_t>> nalInfos;
+
+    const uint8_t *nalStart;
+    size_t nalSize;
+    while (getNextNALUnit(&data, &size, &nalStart, &nalSize, true) == OK) {
+        nalInfos.push_back(
+                std::make_pair(nalStart - accessUnit->data(), nalSize));
+    }
+
+    size_t i = 0;
+    while (i < nalInfos.size()) {
+        size_t totalSize = 12 + 1;
+
+        uint8_t F = 0;
+        uint8_t NRI = 0;
+
+        size_t j = i;
+        while (j < nalInfos.size()) {
+            auto [nalOffset, nalSize] = nalInfos[j];
+
+            size_t fragASize = 2 + nalSize;
+            if (totalSize + fragASize > kMaxSRTPPayloadSize) {
+                break;
+            }
+
+            uint8_t header = accessUnit->data()[nalOffset];
+            F |= (header & 0x80);
+
+            if ((header & 0x60) > NRI) {
+                NRI = header & 0x60;
+            }
+
+            totalSize += fragASize;
+
+            ++j;
+        }
+
+        if (j == i) {
+            // Not even a single NALU fits in a STAP-A packet, but may fit
+            // inside a single-NALU packet...
+
+            auto [nalOffset, nalSize] = nalInfos[i];
+            if (12 + nalSize <= kMaxSRTPPayloadSize) {
+                j = i + 1;
+            }
+        }
+
+        if (j == i) {
+            // Not even a single NALU fits, need an FU-A.
+
+            auto [nalOffset, nalSize] = nalInfos[i];
+
+            uint8_t nalHeader = accessUnit->data()[nalOffset];
+
+            size_t offset = 1;
+            while (offset < nalSize) {
+                size_t copy = std::min(
+                        kMaxSRTPPayloadSize - 12 - 2, nalSize - offset);
+
+                bool last = (offset + copy == nalSize);
+
+                std::vector<uint8_t> packet(12 + 2 + copy);
+
+                uint8_t *data = packet.data();
+                data[0] = 0x80;
+
+                data[1] = PT;
+                if (last && i + 1 == nalInfos.size()) {
+                    data[1] |= 0x80;  // (M)ark
+                }
+
+                SET_U16(&data[2], 0);  // seqNum
+                SET_U32(&data[4], rtpTime);
+                SET_U32(&data[8], SSRC);
+
+                data[12] = (nalHeader & 0xe0) | FU_A;
+
+                data[13] = (nalHeader & 0x1f);
+
+                if (offset == 1) {
+                    CHECK_LT(offset + copy, nalSize);
+                    data[13] |= 0x80;  // (S)tart
+                } else if (last) {
+                    CHECK_GT(offset, 1u);
+                    data[13] |= 0x40;  // (E)nd
+                }
+
+                memcpy(&data[14], accessUnit->data() + nalOffset + offset, copy);
+
+                offset += copy;
+
+                LOG(VERBOSE)
+                    << "Sending FU-A w/ indicator "
+                    << StringPrintf("0x%02x", data[12])
+                    << ", header "
+                    << StringPrintf("0x%02x", data[13]);
+
+                queueRTPDatagram(&packet);
+            }
+
+            ++i;
+            continue;
+        }
+
+        if (j == i + 1) {
+            // Only a single NALU fits.
+
+            auto [nalOffset, nalSize] = nalInfos[i];
+
+            std::vector<uint8_t> packet(12 + nalSize);
+
+            uint8_t *data = packet.data();
+            data[0] = 0x80;
+
+            data[1] = PT;
+            if (i + 1 == nalInfos.size()) {
+                data[1] |= 0x80;  // (M)arker
+            }
+
+            SET_U16(&data[2], 0);  // seqNum
+            SET_U32(&data[4], rtpTime);
+            SET_U32(&data[8], SSRC);
+
+            memcpy(data + 12, accessUnit->data() + nalOffset, nalSize);
+
+            LOG(VERBOSE) << "Sending single NALU of size " << nalSize;
+
+            queueRTPDatagram(&packet);
+
+            ++i;
+            continue;
+        }
+
+        // STAP-A
+
+        std::vector<uint8_t> packet(totalSize);
+
+        uint8_t *data = packet.data();
+        data[0] = 0x80;
+
+        data[1] = PT;
+        if (j == nalInfos.size()) {
+            data[1] |= 0x80;  // (M)arker
+        }
+
+        SET_U16(&data[2], 0);  // seqNum
+        SET_U32(&data[4], rtpTime);
+        SET_U32(&data[8], SSRC);
+
+        data[12] = F | NRI | STAP_A;
+
+        size_t offset = 13;
+        while (i < j) {
+            auto [nalOffset, nalSize] = nalInfos[i];
+
+            SET_U16(&data[offset], nalSize);
+            memcpy(&data[offset + 2], accessUnit->data() + nalOffset, nalSize);
+
+            offset += 2 + nalSize;
+
+            ++i;
+        }
+
+        CHECK_EQ(offset, totalSize);
+
+        LOG(VERBOSE) << "Sending STAP-A of size " << totalSize;
+
+        queueRTPDatagram(&packet);
+    }
+}
+
+uint32_t H264Packetizer::rtpNow() const {
+    if (mNumSamplesRead == 0) {
+        return 0;
+    }
+
+    auto now = std::chrono::steady_clock::now();
+    auto timeSinceStart = now - mStartTimeReal;
+
+    auto us_since_start =
+        std::chrono::duration_cast<std::chrono::microseconds>(
+                timeSinceStart).count();
+
+    return (us_since_start * 9) / 100;
+}
+
+android::status_t H264Packetizer::requestIDRFrame() {
+    return mFrameBufferSource->requestIDRFrame();
+}
+
diff --git a/host/frontend/gcastv2/webrtc/MyWebSocketHandler.cpp b/host/frontend/gcastv2/webrtc/MyWebSocketHandler.cpp
new file mode 100644
index 0000000..3e38dfb
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/MyWebSocketHandler.cpp
@@ -0,0 +1,661 @@
+#include <webrtc/MyWebSocketHandler.h>
+
+#include "Utils.h"
+
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/foundation/hexdump.h>
+#include <media/stagefright/foundation/JSONObject.h>
+#include <media/stagefright/Utils.h>
+
+#include <netdb.h>
+#include <openssl/rand.h>
+
+template<class T> using sp = android::sp<T>;
+using android::JSONValue;
+using android::JSONObject;
+using android::ABuffer;
+
+MyWebSocketHandler::MyWebSocketHandler(
+        std::shared_ptr<RunLoop> runLoop,
+        std::shared_ptr<ServerState> serverState,
+        size_t handlerId)
+    : mRunLoop(runLoop),
+      mServerState(serverState),
+      mId(handlerId),
+      mOptions(OptionBits::useSingleCertificateForAllTracks),
+      mTouchSink(mServerState->getTouchSink()) {
+}
+
+MyWebSocketHandler::~MyWebSocketHandler() {
+    for (auto rtp : mRTPs) {
+        mServerState->releasePort(rtp->getLocalPort());
+    }
+
+    mServerState->releaseHandlerId(mId);
+}
+
+int MyWebSocketHandler::handleMessage(
+        uint8_t /* headerByte */, const uint8_t *msg, size_t len) {
+    // android::hexdump(msg, len);
+
+    JSONValue json;
+    if (JSONValue::Parse(
+                reinterpret_cast<const char *>(msg), len, &json) < 0) {
+        return -EINVAL;
+    }
+
+    sp<JSONObject> obj;
+    if (!json.getObject(&obj)) {
+        return -EINVAL;
+    }
+
+    LOG(VERBOSE) << obj->toString();
+
+    std::string type;
+    if (!obj->getString("type", &type)) {
+        return -EINVAL;
+    }
+
+    if (type == "greeting") {
+        sp<JSONObject> reply = new JSONObject;
+        reply->setString("type", "hello");
+        reply->setString("reply", "Right back at ya!");
+
+        auto replyAsString = reply->toString();
+        sendMessage(replyAsString.c_str(), replyAsString.size());
+
+        std::string value;
+        if (obj->getString("path", &value)) {
+            parseOptions(value);
+        }
+
+        if (mOptions & OptionBits::useSingleCertificateForAllTracks) {
+            mCertificateAndKey = CreateDTLSCertificateAndKey();
+        }
+
+        prepareSessions();
+    } else if (type == "set-remote-desc") {
+        std::string value;
+        if (!obj->getString("sdp", &value)) {
+            return -EINVAL;
+        }
+
+        int err = mOfferedSDP.setTo(value);
+
+        if (err) {
+            LOG(ERROR) << "Offered SDP could not be parsed (" << err << ")";
+        }
+
+        for (size_t i = 0; i < mSessions.size(); ++i) {
+            const auto &session = mSessions[i];
+
+            session->setRemoteParams(
+                    getRemoteUFrag(i),
+                    getRemotePassword(i),
+                    getRemoteFingerprint(i));
+        }
+
+        return err;
+    } else if (type == "request-offer") {
+        std::stringstream ss;
+
+        ss <<
+"v=0\r\n"
+"o=- 7794515898627856655 2 IN IP4 127.0.0.1\r\n"
+"s=-\r\n"
+"t=0 0\r\n"
+"a=msid-semantic: WMS pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw\r\n";
+
+        bool bundled = false;
+
+        if ((mOptions & OptionBits::bundleTracks) && countTracks() > 1) {
+            bundled = true;
+
+            ss << "a=group:BUNDLE 0";
+
+            if (!(mOptions & OptionBits::disableAudio)) {
+                ss << " 1";
+            }
+
+            if (mOptions & OptionBits::enableData) {
+                ss << " 2";
+            }
+
+            ss << "\r\n";
+
+            emitTrackIceOptionsAndFingerprint(ss, 0 /* mlineIndex */);
+        }
+
+        size_t mlineIndex = 0;
+
+        // Video track (mid = 0)
+
+        std::string videoEncodingSpecific =
+            (mServerState->videoFormat() == ServerState::VideoFormat::H264) ?
+"a=rtpmap:96 H264/90000\r\n"
+"a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n"
+            :
+"a=rtpmap:96 VP8/90000\r\n";
+
+        videoEncodingSpecific +=
+"a=rtcp-fb:96 ccm fir\r\n"
+"a=rtcp-fb:96 nack\r\n"
+"a=rtcp-fb:96 nack pli\r\n";
+
+        ss <<
+"m=video 9 UDP/TLS/RTP/SAVPF 96 97\r\n"
+"c=IN IP4 0.0.0.0\r\n"
+"a=rtcp:9 IN IP4 0.0.0.0\r\n";
+
+        if (!bundled) {
+            emitTrackIceOptionsAndFingerprint(ss, mlineIndex++);
+        }
+
+        ss <<
+"a=setup:actpass\r\n"
+"a=mid:0\r\n"
+"a=sendonly\r\n"
+"a=rtcp-mux\r\n"
+"a=rtcp-rsize\r\n"
+"a=rtcp-xr:rcvr-rtt=all\r\n";
+
+        ss << videoEncodingSpecific <<
+"a=rtpmap:97 rtx/90000\r\n"
+"a=fmtp:97 apt=96\r\n"
+"a=ssrc-group:FID 3735928559 3405689008\r\n"
+"a=ssrc:3735928559 cname:myWebRTP\r\n"
+"a=ssrc:3735928559 msid:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw 61843855-edd7-4ca9-be79-4e3ccc6cc035\r\n"
+"a=ssrc:3735928559 mslabel:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw\r\n"
+"a=ssrc:3735928559 label:61843855-edd7-4ca9-be79-4e3ccc6cc035\r\n"
+"a=ssrc:3405689008 cname:myWebRTP\r\n"
+"a=ssrc:3405689008 msid:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw 61843855-edd7-4ca9-be79-4e3ccc6cc035\r\n"
+"a=ssrc:3405689008 mslabel:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw\r\n"
+"a=ssrc:3405689008 label:61843855-edd7-4ca9-be79-4e3ccc6cc035\r\n";
+
+        if (!(mOptions & OptionBits::disableAudio)) {
+            ss <<
+"m=audio 9 UDP/TLS/RTP/SAVPF 98\r\n"
+"c=IN IP4 0.0.0.0\r\n"
+"a=rtcp:9 IN IP4 0.0.0.0\r\n";
+
+            if (!bundled) {
+                emitTrackIceOptionsAndFingerprint(ss, mlineIndex++);
+            }
+
+            ss <<
+"a=setup:actpass\r\n"
+"a=mid:1\r\n"
+"a=sendonly\r\n"
+"a=msid:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw 61843856-edd7-4ca9-be79-4e3ccc6cc035\r\n"
+"a=rtcp-mux\r\n"
+"a=rtcp-rsize\r\n"
+"a=rtpmap:98 opus/48000/2\r\n"
+"a=fmtp:98 minptime=10;useinbandfec=1\r\n"
+"a=ssrc-group:FID 2343432205\r\n"
+"a=ssrc:2343432205 cname:myWebRTP\r\n"
+"a=ssrc:2343432205 msid:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw 61843856-edd7-4ca9-be79-4e3ccc6cc035\r\n"
+"a=ssrc:2343432205 mslabel:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw\r\n"
+"a=ssrc:2343432205 label:61843856-edd7-4ca9-be79-4e3ccc6cc035\r\n";
+        }
+
+        if (mOptions & OptionBits::enableData) {
+            ss <<
+"m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n"
+"c=IN IP4 0.0.0.0\r\n"
+"a=sctp-port:5000\r\n";
+
+            if (!bundled) {
+                emitTrackIceOptionsAndFingerprint(ss, mlineIndex++);
+            }
+
+            ss <<
+"a=setup:actpass\r\n"
+"a=mid:2\r\n"
+"a=sendrecv\r\n"
+"a=fmtp:webrtc-datachannel max-message-size=65536\r\n";
+        }
+
+        sp<JSONObject> reply = new JSONObject;
+        reply->setString("type", "offer");
+        reply->setString("sdp", ss.str());
+
+        auto replyAsString = reply->toString();
+        sendMessage(replyAsString.c_str(), replyAsString.size());
+    } else if (type == "get-ice-candidate") {
+        int32_t mid;
+        CHECK(obj->getInt32("mid", &mid));
+
+        bool success = getCandidate(mid);
+
+        if (!success) {
+            sp<JSONObject> reply = new JSONObject;
+            reply->setString("type", "ice-candidate");
+
+            auto replyAsString = reply->toString();
+            sendMessage(replyAsString.c_str(), replyAsString.size());
+        }
+    } else if (type == "set-mouse-position") {
+        int32_t down;
+        CHECK(obj->getInt32("down", &down));
+
+        int32_t x, y;
+        CHECK(obj->getInt32("x", &x));
+        CHECK(obj->getInt32("y", &y));
+
+        LOG(VERBOSE)
+            << "set-mouse-position(" << down << ", " << x << ", " << y << ")";
+
+        sp<ABuffer> accessUnit = new ABuffer(3 * sizeof(int32_t));
+        int32_t *data = reinterpret_cast<int32_t *>(accessUnit->data());
+        data[0] = down;
+        data[1] = (x * 720) / 360;
+        data[2] = (y * 1440) / 720;
+
+        mTouchSink->onAccessUnit(accessUnit);
+    } else if (type == "inject-multi-touch") {
+        int32_t id, initialDown, x, y, slot;
+        CHECK(obj->getInt32("id", &id));
+        CHECK(obj->getInt32("initialDown", &initialDown));
+        CHECK(obj->getInt32("x", &x));
+        CHECK(obj->getInt32("y", &y));
+        CHECK(obj->getInt32("slot", &slot));
+
+        LOG(VERBOSE)
+            << "inject-multi-touch id="
+            << id
+            << ", initialDown="
+            << initialDown
+            << ", x="
+            << x
+            << ", y="
+            << y
+            << ", slot="
+            << slot;
+
+        sp<ABuffer> accessUnit = new ABuffer(5 * sizeof(int32_t));
+        int32_t *data = reinterpret_cast<int32_t *>(accessUnit->data());
+        data[0] = id;
+        data[1] = (initialDown != 0);
+        data[2] = (x * 720) / 360;
+        data[3] = (y * 1440) / 720;
+        data[4] = slot;
+
+        mTouchSink->onAccessUnit(accessUnit);
+    }
+
+    return 0;
+}
+
+size_t MyWebSocketHandler::countTracks() const {
+    size_t n = 1;  // We always have a video track.
+
+    if (!(mOptions & OptionBits::disableAudio)) {
+        ++n;
+    }
+
+    if (mOptions & OptionBits::enableData) {
+        ++n;
+    }
+
+    return n;
+}
+
+ssize_t MyWebSocketHandler::mlineIndexForMid(int32_t mid) const {
+    switch (mid) {
+        case 0:
+            return 0;
+
+        case 1:
+            if (mOptions & OptionBits::disableAudio) {
+                return -1;
+            }
+
+            return 1;
+
+        case 2:
+            if (!(mOptions & OptionBits::enableData)) {
+                return -1;
+            }
+
+            if (mOptions & OptionBits::disableAudio) {
+                return 1;
+            }
+
+            return 2;
+
+        default:
+            return -1;
+    }
+}
+
+bool MyWebSocketHandler::getCandidate(int32_t mid) {
+    auto mlineIndex = mlineIndexForMid(mid);
+
+    if (mlineIndex < 0) {
+        return false;
+    }
+
+    if (!(mOptions & OptionBits::bundleTracks) || mRTPs.empty()) {
+        // Only allocate a local port once if we bundle tracks.
+
+        auto localPort = mServerState->acquirePort();
+
+        if (!localPort) {
+            return false;
+        }
+
+        size_t sessionIndex = mlineIndex;
+
+        uint32_t trackMask = 0;
+        if (mOptions & OptionBits::bundleTracks) {
+            sessionIndex = 0;  // One session for all tracks.
+
+            trackMask = RTPSocketHandler::TRACK_VIDEO;
+
+            if (!(mOptions & OptionBits::disableAudio)) {
+                trackMask |= RTPSocketHandler::TRACK_AUDIO;
+            }
+
+            if (mOptions & OptionBits::enableData) {
+                trackMask |= RTPSocketHandler::TRACK_DATA;
+            }
+        } else if (mid == 0) {
+            trackMask = RTPSocketHandler::TRACK_VIDEO;
+        } else if (mid == 1) {
+            trackMask = RTPSocketHandler::TRACK_AUDIO;
+        } else {
+            trackMask = RTPSocketHandler::TRACK_DATA;
+        }
+
+        const auto &session = mSessions[sessionIndex];
+
+        auto rtp = std::make_shared<RTPSocketHandler>(
+                mRunLoop,
+                mServerState,
+                PF_INET,
+                localPort,
+                trackMask,
+                session);
+
+        rtp->run();
+
+        mRTPs.push_back(rtp);
+    }
+
+    auto rtp = mRTPs.back();
+
+    sp<JSONObject> reply = new JSONObject;
+    reply->setString("type", "ice-candidate");
+
+    auto localIPString = rtp->getLocalIPString();
+
+    // see rfc8445, 5.1.2.1. for the derivation of "2122121471" below.
+    reply->setString(
+            "candidate",
+                "candidate:0 1 UDP 2122121471 "
+                + localIPString
+                + " "
+                + std::to_string(rtp->getLocalPort())
+                + " typ host generation 0 ufrag "
+                + rtp->getLocalUFrag());
+
+    reply->setInt32("mlineIndex", mlineIndex);
+
+    auto replyAsString = reply->toString();
+    sendMessage(replyAsString.c_str(), replyAsString.size());
+
+    return true;
+}
+
+std::optional<std::string> MyWebSocketHandler::getSDPValue(
+        ssize_t targetMediaIndex,
+        std::string_view key,
+        bool fallthroughToGeneralSection) const {
+
+    CHECK_GE(targetMediaIndex, -1);
+
+    if (targetMediaIndex + 1 >= mOfferedSDP.countSections()) {
+        LOG(ERROR)
+            << "getSDPValue: targetMediaIndex "
+            << targetMediaIndex
+            << " out of range (countSections()="
+            << mOfferedSDP.countSections()
+            << ")";
+
+        return std::nullopt;
+    }
+
+    const std::string prefix = "a=" + std::string(key) + ":";
+
+    auto sectionIndex = 1 + targetMediaIndex;
+    auto rangeEnd = mOfferedSDP.section_end(sectionIndex);
+
+    auto it = std::find_if(
+            mOfferedSDP.section_begin(sectionIndex),
+            rangeEnd,
+            [prefix](const auto &line) {
+        return StartsWith(line, prefix);
+    });
+
+    if (it == rangeEnd) {
+        if (fallthroughToGeneralSection) {
+            CHECK_NE(targetMediaIndex, -1);
+
+            // Oh no, scary recursion ahead.
+            return getSDPValue(
+                    -1 /* targetMediaIndex */,
+                    key,
+                    false /* fallthroughToGeneralSection */);
+        }
+
+        LOG(WARNING)
+            << "Unable to find '"
+            << prefix
+            << "' with targetMediaIndex="
+            << targetMediaIndex;
+
+        return std::nullopt;
+    }
+
+    return (*it).substr(prefix.size());
+}
+
+std::string MyWebSocketHandler::getRemotePassword(size_t mlineIndex) const {
+    auto value = getSDPValue(
+            mlineIndex, "ice-pwd", true /* fallthroughToGeneralSection */);
+
+    return value ? *value : std::string();
+}
+
+std::string MyWebSocketHandler::getRemoteUFrag(size_t mlineIndex) const {
+    auto value = getSDPValue(
+            mlineIndex, "ice-ufrag", true /* fallthroughToGeneralSection */);
+
+    return value ? *value : std::string();
+}
+
+std::string MyWebSocketHandler::getRemoteFingerprint(size_t mlineIndex) const {
+    auto value = getSDPValue(
+            mlineIndex, "fingerprint", true /* fallthroughToGeneralSection */);
+
+    return value ? *value : std::string();
+}
+
+// static
+std::pair<std::shared_ptr<X509>, std::shared_ptr<EVP_PKEY>>
+MyWebSocketHandler::CreateDTLSCertificateAndKey() {
+    // Modeled after "https://stackoverflow.com/questions/256405/
+    // programmatically-create-x509-certificate-using-openssl".
+
+    std::shared_ptr<EVP_PKEY> pkey(EVP_PKEY_new(), EVP_PKEY_free);
+
+    std::unique_ptr<RSA, std::function<void(RSA *)>> rsa(
+            RSA_new(), RSA_free);
+
+    BIGNUM exponent;
+    BN_init(&exponent);
+    BN_set_word(&exponent, RSA_F4);
+
+    int res = RSA_generate_key_ex(
+            rsa.get() /* rsa */, 2048, &exponent, nullptr /* callback */);
+
+    CHECK_EQ(res, 1);
+
+    EVP_PKEY_assign_RSA(pkey.get(), rsa.release());
+
+    std::shared_ptr<X509> x509(X509_new(), X509_free);
+
+    ASN1_INTEGER_set(X509_get_serialNumber(x509.get()), 1);
+
+    X509_gmtime_adj(X509_get_notBefore(x509.get()), 0);
+    X509_gmtime_adj(X509_get_notAfter(x509.get()), 60 * 60 * 24 * 7); // 7 days.
+
+    X509_set_pubkey(x509.get(), pkey.get());
+
+    X509_NAME *name = X509_get_subject_name(x509.get());
+
+    X509_NAME_add_entry_by_txt(
+            name, "C",  MBSTRING_ASC, (unsigned char *)"US", -1, -1, 0);
+
+    X509_NAME_add_entry_by_txt(
+            name,
+            "O",
+            MBSTRING_ASC,
+            (unsigned char *)"Beyond Aggravated",
+            -1,
+            -1,
+            0);
+
+    X509_NAME_add_entry_by_txt(
+            name, "CN", MBSTRING_ASC, (unsigned char *)"localhost", -1, -1, 0);
+
+    X509_set_issuer_name(x509.get(), name);
+
+    auto digest = EVP_sha256();
+
+    X509_sign(x509.get(), pkey.get(), digest);
+
+    return std::make_pair(x509, pkey);
+}
+
+void MyWebSocketHandler::parseOptions(const std::string &pathAndQuery) {
+    auto separatorPos = pathAndQuery.find("?");
+
+    if (separatorPos == std::string::npos) {
+        return;
+    }
+
+    auto components = SplitString(pathAndQuery.substr(separatorPos + 1), '&');
+    for (auto name : components) {
+        bool boolValue = true;
+
+        separatorPos = name.find("=");
+        if (separatorPos != std::string::npos) {
+            boolValue = false;
+
+            auto value = name.substr(separatorPos + 1);
+            name.erase(separatorPos);
+
+            boolValue =
+                !strcasecmp("true", value.c_str())
+                    || !strcasecmp("yes", value.c_str())
+                    || value == "1";
+        }
+
+        if (name == "disable_audio") {
+            auto mask = OptionBits::disableAudio;
+            mOptions = (mOptions & ~mask) | (boolValue ? mask : 0);
+        } else if (name == "bundle_tracks" && boolValue) {
+            auto mask = OptionBits::bundleTracks;
+            mOptions = (mOptions & ~mask) | (boolValue ? mask : 0);
+        } else if (name == "enable_data" && boolValue) {
+            auto mask = OptionBits::enableData;
+            mOptions = (mOptions & ~mask) | (boolValue ? mask : 0);
+        }
+    }
+}
+
+// static
+void MyWebSocketHandler::CreateRandomIceCharSequence(char *dst, size_t size) {
+    // Per RFC 5245 an ice-char is alphanumeric, '+' or '/', i.e. 64 distinct
+    // character values (6 bit).
+
+    CHECK_EQ(1, RAND_bytes(reinterpret_cast<unsigned char *>(dst), size));
+
+    for (size_t i = 0; i < size; ++i) {
+        char x = dst[i] & 0x3f;
+        if (x < 26) {
+            x += 'a';
+        } else if (x < 52) {
+            x += 'A' - 26;
+        } else if (x < 62) {
+            x += '0' - 52;
+        } else if (x < 63) {
+            x = '+';
+        } else {
+            x = '/';
+        }
+
+        dst[i] = x;
+    }
+}
+
+std::pair<std::string, std::string>
+MyWebSocketHandler::createUniqueUFragAndPassword() {
+    // RFC 5245, section 15.4 mandates that uFrag is at least 4 and password
+    // at least 22 ice-chars long.
+
+    char uFragChars[4];
+
+    for (;;) {
+        CreateRandomIceCharSequence(uFragChars, sizeof(uFragChars));
+
+        std::string uFrag(uFragChars, sizeof(uFragChars));
+
+        auto it = std::find_if(
+                mSessions.begin(), mSessions.end(),
+                [uFrag](const auto &session) {
+                    return session->localUFrag() == uFrag;
+                });
+
+        if (it == mSessions.end()) {
+            // This uFrag is not in use yet.
+            break;
+        }
+    }
+
+    char passwordChars[22];
+    CreateRandomIceCharSequence(passwordChars, sizeof(passwordChars));
+
+    return std::make_pair(
+            std::string(uFragChars, sizeof(uFragChars)),
+            std::string(passwordChars, sizeof(passwordChars)));
+}
+
+void MyWebSocketHandler::prepareSessions() {
+    size_t numSessions =
+        (mOptions & OptionBits::bundleTracks) ? 1 : countTracks();
+
+    for (size_t i = 0; i < numSessions; ++i) {
+        auto [ufrag, password] = createUniqueUFragAndPassword();
+
+        auto [certificate, key] =
+            (mOptions & OptionBits::useSingleCertificateForAllTracks)
+                ? mCertificateAndKey : CreateDTLSCertificateAndKey();
+
+        mSessions.push_back(
+                std::make_shared<RTPSession>(
+                    ufrag, password, certificate, key));
+    }
+}
+
+void MyWebSocketHandler::emitTrackIceOptionsAndFingerprint(
+        std::stringstream &ss, size_t mlineIndex) const {
+    CHECK_LT(mlineIndex, mSessions.size());
+    const auto &session = mSessions[mlineIndex];
+
+    ss << "a=ice-ufrag:" << session->localUFrag() << "\r\n";
+    ss << "a=ice-pwd:" << session->localPassword() << "\r\n";
+    ss << "a=ice-options:trickle\r\n";
+    ss << "a=fingerprint:" << session->localFingerprint() << "\r\n";
+}
diff --git a/host/frontend/gcastv2/webrtc/OpusPacketizer.cpp b/host/frontend/gcastv2/webrtc/OpusPacketizer.cpp
new file mode 100644
index 0000000..5331ec1
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/OpusPacketizer.cpp
@@ -0,0 +1,124 @@
+#include <webrtc/OpusPacketizer.h>
+
+#include "Utils.h"
+
+#include <webrtc/RTPSocketHandler.h>
+
+#include <https/SafeCallbackable.h>
+#include <media/stagefright/foundation/AMessage.h>
+#include <media/stagefright/Utils.h>
+
+using namespace android;
+
+OpusPacketizer::OpusPacketizer(
+        std::shared_ptr<RunLoop> runLoop,
+        std::shared_ptr<StreamingSource> audioSource)
+    : mRunLoop(runLoop),
+      mAudioSource(audioSource),
+      mNumSamplesRead(0),
+      mStartTimeMedia(0),
+      mFirstInTalkspurt(true) {
+}
+
+void OpusPacketizer::run() {
+    auto weak_this = std::weak_ptr<OpusPacketizer>(shared_from_this());
+
+    mAudioSource->setCallback(
+            [weak_this](const sp<ABuffer> &accessUnit) {
+                auto me = weak_this.lock();
+                if (me) {
+                    me->mRunLoop->post(
+                            makeSafeCallback(
+                                me.get(), &OpusPacketizer::onFrame, accessUnit));
+                }
+            });
+
+    mAudioSource->start();
+}
+
+void OpusPacketizer::onFrame(const sp<ABuffer> &accessUnit) {
+    int64_t timeUs;
+    CHECK(accessUnit->meta()->findInt64("timeUs", &timeUs));
+
+    auto now = std::chrono::steady_clock::now();
+
+    if (mNumSamplesRead == 0) {
+        mStartTimeMedia = timeUs;
+        mStartTimeReal = now;
+    }
+
+    ++mNumSamplesRead;
+
+    LOG(VERBOSE)
+        << "got accessUnit of size "
+        << accessUnit->size()
+        << " at time "
+        << timeUs;
+
+    packetize(accessUnit, timeUs);
+}
+
+void OpusPacketizer::packetize(const sp<ABuffer> &accessUnit, int64_t timeUs) {
+    LOG(VERBOSE) << "Received Opus frame of size " << accessUnit->size();
+
+    static constexpr uint8_t PT = 98;
+    static constexpr uint32_t SSRC = 0x8badf00d;
+
+    // XXX Retransmission packets add 2 bytes (for the original seqNum), should
+    // probably reserve that amount in the original packets so we don't exceed
+    // the MTU on retransmission.
+    static const size_t kMaxSRTPPayloadSize =
+        RTPSocketHandler::kMaxUDPPayloadSize - SRTP_MAX_TRAILER_LEN;
+
+    const uint8_t *audioData = accessUnit->data();
+    size_t size = accessUnit->size();
+
+    uint32_t rtpTime = ((timeUs - mStartTimeMedia) * 48) / 1000;
+
+#if 0
+    static uint32_t lastRtpTime = 0;
+    LOG(INFO) << "rtpTime = " << rtpTime << " [+" << (rtpTime - lastRtpTime) << "]";
+    lastRtpTime = rtpTime;
+#endif
+
+    CHECK_LE(12 + size, kMaxSRTPPayloadSize);
+
+    std::vector<uint8_t> packet(12 + size);
+    uint8_t *data = packet.data();
+
+    packet[0] = 0x80;
+    packet[1] = PT;
+
+    if (mFirstInTalkspurt) {
+        packet[1] |= 0x80;  // (M)ark
+        mFirstInTalkspurt = false;
+    }
+
+    SET_U16(&data[2], 0);  // seqNum
+    SET_U32(&data[4], rtpTime);
+    SET_U32(&data[8], SSRC);
+
+    memcpy(&data[12], audioData, size);
+
+    queueRTPDatagram(&packet);
+}
+
+uint32_t OpusPacketizer::rtpNow() const {
+    if (mNumSamplesRead == 0) {
+        return 0;
+    }
+
+    auto now = std::chrono::steady_clock::now();
+    auto timeSinceStart = now - mStartTimeReal;
+
+    auto us_since_start =
+        std::chrono::duration_cast<std::chrono::microseconds>(
+                timeSinceStart).count();
+
+    return (us_since_start * 48) / 1000;
+}
+
+android::status_t OpusPacketizer::requestIDRFrame() {
+    return mAudioSource->requestIDRFrame();
+}
+
diff --git a/host/frontend/gcastv2/webrtc/Packetizer.cpp b/host/frontend/gcastv2/webrtc/Packetizer.cpp
new file mode 100644
index 0000000..19d5ee4
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/Packetizer.cpp
@@ -0,0 +1,22 @@
+#include <webrtc/Packetizer.h>
+
+#include <webrtc/RTPSender.h>
+
+void Packetizer::queueRTPDatagram(std::vector<uint8_t> *packet) {
+    auto it = mSenders.begin(); 
+    while (it != mSenders.end()) {
+        auto sender = it->lock();
+        if (!sender) {
+            it = mSenders.erase(it);
+            continue;
+        }
+
+        sender->queueRTPDatagram(packet);
+        ++it;
+    }
+}
+
+void Packetizer::addSender(std::shared_ptr<RTPSender> sender) {
+    mSenders.push_back(sender);
+}
+
diff --git a/host/frontend/gcastv2/webrtc/RTPSender.cpp b/host/frontend/gcastv2/webrtc/RTPSender.cpp
new file mode 100644
index 0000000..36ef041
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/RTPSender.cpp
@@ -0,0 +1,576 @@
+#include <webrtc/RTPSender.h>
+
+#include "Utils.h"
+
+#include <webrtc/RTPSocketHandler.h>
+
+#include <https/SafeCallbackable.h>
+#include <media/stagefright/foundation/hexdump.h>
+#include <media/stagefright/Utils.h>
+
+#include <random>
+#include <unordered_set>
+
+using android::U16_AT;
+using android::U32_AT;
+using android::StringPrintf;
+
+#define SIMULATE_PACKET_LOSS    0
+
+RTPSender::RTPSender(
+        std::shared_ptr<RunLoop> runLoop,
+        RTPSocketHandler *parent,
+        std::shared_ptr<Packetizer> videoPacketizer,
+        std::shared_ptr<Packetizer> audioPacketizer)
+    : mRunLoop(runLoop),
+      mParent(parent),
+      mVideoPacketizer(videoPacketizer),
+      mAudioPacketizer(audioPacketizer) {
+}
+
+void RTPSender::addSource(uint32_t ssrc) {
+    CHECK(mSources.insert(
+                std::make_pair(ssrc, SourceInfo())).second);
+}
+
+void RTPSender::addRetransInfo(
+        uint32_t ssrc, uint8_t PT, uint32_t retransSSRC, uint8_t retransPT) {
+    auto it = mSources.find(ssrc);
+    CHECK(it != mSources.end());
+
+    auto &info = it->second;
+
+    CHECK(info.mRetrans.insert(
+                std::make_pair(
+                    PT, std::make_pair(retransSSRC, retransPT))).second);
+}
+
+int RTPSender::injectRTCP(uint8_t *data, size_t size) {
+    // LOG(INFO) << "RTPSender::injectRTCP";
+    // android::hexdump(data, size);
+
+    while (size > 0) {
+        if (size < 8) {
+            return -EINVAL;
+        }
+
+        if ((data[0] >> 6) != 2) {
+            // Wrong version.
+            return -EINVAL;
+        }
+
+        size_t lengthInWords = U16_AT(&data[2]) + 1;
+
+        bool hasPadding = (data[0] & 0x20);
+
+        size_t headerSize = 4 * lengthInWords;
+
+        if (size < headerSize) {
+            return -EINVAL;
+        }
+
+        if (hasPadding) {
+            if (size != headerSize) {
+                // Padding should only be added to the last packet in a compound
+                // packet.
+                return -EINVAL;
+            }
+
+            size_t numPadBytes = data[headerSize - 1];
+            if (numPadBytes == 0 || (numPadBytes % 4) != 0) {
+                return -EINVAL;
+            }
+
+            headerSize -= numPadBytes;
+        }
+
+        auto err = processRTCP(data, headerSize);
+
+        if (err) {
+            return err;
+        }
+
+        data += 4 * lengthInWords;
+        size -= 4 * lengthInWords;
+    }
+
+    return 0;
+}
+
+int RTPSender::processRTCP(const uint8_t *data, size_t size) {
+    static constexpr uint8_t RR = 201;     // RFC 3550
+    // static constexpr uint8_t SDES = 202;
+    // static constexpr uint8_t BYE = 203;
+    // static constexpr uint8_t APP = 204;
+    static constexpr uint8_t RTPFB = 205;  // RFC 4585
+    static constexpr uint8_t PSFB = 206;
+    static constexpr uint8_t XR = 207;  // RFC 3611
+
+#if 0
+    LOG(INFO) << "RTPSender::processRTCP";
+    android::hexdump(data, size);
+#endif
+
+    unsigned PT = data[1];
+
+    switch (PT) {
+        case RR:
+        {
+            unsigned RC = data[0] & 0x1f;
+            if (size != 8 + RC * 6 * 4) {
+                return -EINVAL;
+            }
+
+            auto senderSSRC = U32_AT(&data[4]);
+
+            size_t offset = 8;
+            for (unsigned i = 0; i < RC; ++i) {
+                auto SSRC = U32_AT(&data[offset]);
+                auto fractionLost = data[offset + 4];
+                auto cumPacketsLost = U32_AT(&data[offset + 4]) & 0xffffff;
+
+                if (fractionLost) {
+                    LOG(INFO)
+                        << "sender SSRC "
+                        << StringPrintf("0x%08x", senderSSRC)
+                        << " reports "
+                        << StringPrintf("%.2f %%", (double)fractionLost * 100.0 / 256.0)
+                        << " lost, cum. total: "
+                        << cumPacketsLost
+                        << " from SSRC "
+                        << StringPrintf("0x%08x", SSRC);
+                }
+
+                offset += 6 * 4;
+            }
+            break;
+        }
+
+        case RTPFB:
+        {
+            static constexpr uint8_t NACK = 1;
+
+            if (size < 12) {
+                return -EINVAL;
+            }
+
+            unsigned fmt = data[0] & 0x1f;
+
+            auto senderSSRC = U32_AT(&data[4]);
+            auto SSRC = U32_AT(&data[8]);
+
+            switch (fmt) {
+                case NACK:
+                {
+                    size_t offset = 12;
+                    size_t n = (size - offset) / 4;
+                    for (size_t i = 0; i < n; ++i) {
+                        auto PID = U16_AT(&data[offset]);
+                        auto BLP = U16_AT(&data[offset + 2]);
+
+                        LOG(INFO)
+                            << "SSRC "
+                            << StringPrintf("0x%08x", senderSSRC)
+                            << " reports NACK w/ PID="
+                            << StringPrintf("0x%04x", PID)
+                            << ", BLP="
+                            << StringPrintf("0x%04x", BLP)
+                            << " from SSRC "
+                            << StringPrintf("0x%08x", SSRC);
+
+                        offset += 4;
+
+                        retransmitPackets(SSRC, PID, BLP);
+                    }
+                    break;
+                }
+
+                default:
+                {
+                    LOG(WARNING) << "RTPSender::processRTCP unhandled RTPFB.";
+                    android::hexdump(data, size);
+                    break;
+                }
+            }
+
+            break;
+        }
+
+        case PSFB:
+        {
+            static constexpr uint8_t FMT_PLI = 1;
+            static constexpr uint8_t FMT_SLI = 2;
+            static constexpr uint8_t FMT_AFB = 15;
+
+            if (size < 12) {
+                return -EINVAL;
+            }
+
+            unsigned fmt = data[0] & 0x1f;
+
+            auto SSRC = U32_AT(&data[4]);
+
+            switch (fmt) {
+                case FMT_PLI:
+                {
+                    if (size != 12) {
+                        return -EINVAL;
+                    }
+
+                    LOG(INFO)
+                        << "Received PLI from SSRC "
+                        << StringPrintf("0x%08x", SSRC);
+
+                    if (mVideoPacketizer) {
+                        mVideoPacketizer->requestIDRFrame();
+                    }
+                    break;
+                }
+
+                case FMT_SLI:
+                {
+                    LOG(INFO)
+                        << "Received SLI from SSRC "
+                        << StringPrintf("0x%08x", SSRC);
+
+                    break;
+                }
+
+                case FMT_AFB:
+                    break;
+
+                default:
+                {
+                    LOG(WARNING) << "RTPSender::processRTCP unhandled PSFB.";
+                    android::hexdump(data, size);
+                    break;
+                }
+            }
+            break;
+        }
+
+        case XR:
+        {
+            static constexpr uint8_t FMT_RRTRB = 4;
+
+            if (size < 8) {
+                return -EINVAL;
+            }
+
+            auto senderSSRC = U32_AT(&data[4]);
+
+            size_t offset = 8;
+            while (offset + 3 < size) {
+                auto fmt = data[offset];
+                auto blockLength = 4 * (1 + U16_AT(&data[offset + 2]));
+
+                if (offset + blockLength > size) {
+                    LOG(WARNING) << "Found incomplete XR report block.";
+                    break;
+                }
+
+                switch (fmt) {
+                    case FMT_RRTRB:
+                    {
+                        if (blockLength != 12) {
+                            LOG(WARNING)
+                                << "Found XR-RRTRB block of invalid length.";
+                            break;
+                        }
+
+                        auto ntpHi = U32_AT(&data[offset + 4]);
+                        auto ntpLo = U32_AT(&data[offset + 8]);
+
+                        queueDLRR(
+                                0xdeadbeef /* localSSRC */,
+                                senderSSRC,
+                                ntpHi,
+                                ntpLo);
+                        break;
+                    }
+
+                    default:
+                    {
+                        LOG(WARNING)
+                            << "Ignoring unknown XR block type " << fmt;
+
+                        break;
+                    }
+                }
+
+                offset += blockLength;
+            }
+
+            if (offset != size) {
+                LOG(WARNING) << "Found trailing bytes in XR report.";
+            }
+            break;
+        }
+
+        default:
+        {
+            LOG(WARNING) << "RTPSender::processRTCP unhandled packet type.";
+            android::hexdump(data, size);
+        }
+    }
+
+    return 0;
+}
+
+void RTPSender::appendSR(std::vector<uint8_t> *buffer, uint32_t localSSRC) {
+    static constexpr uint8_t SR = 200;
+
+    auto it = mSources.find(localSSRC);
+    CHECK(it != mSources.end());
+
+    const auto &info = it->second;
+
+    const size_t kLengthInWords = 7;
+
+    auto offset = buffer->size();
+    buffer->resize(offset + kLengthInWords * sizeof(uint32_t));
+
+    uint8_t *data = buffer->data() + offset;
+
+    data[0] = 0x80;
+    data[1] = SR;
+    SET_U16(&data[2], kLengthInWords - 1);
+    SET_U32(&data[4], localSSRC);
+
+    auto now = std::chrono::system_clock::now();
+
+    auto us_since_epoch =
+        std::chrono::duration_cast<std::chrono::microseconds>(
+            now.time_since_epoch()).count();
+
+    // This assumes that sd::chrono::system_clock's epoch is unix epoch, i.e.
+    // 1/1/1970 midnight UTC.
+    // Microseconds between midnight 1/1/1970 and midnight 1/1/1900.
+    us_since_epoch += 2208988800ULL * 1000ull;
+
+    uint64_t ntpHi = us_since_epoch / 1000000ll;
+    uint64_t ntpLo = ((1LL << 32) * (us_since_epoch % 1000000LL)) / 1000000LL;
+
+    uint32_t rtpNow =
+        (localSSRC == 0xdeadbeef || localSSRC == 0xcafeb0b0)
+            ? mVideoPacketizer->rtpNow()
+            : mAudioPacketizer->rtpNow();
+
+    SET_U32(&data[8], ntpHi);
+    SET_U32(&data[12], ntpLo);
+    SET_U32(&data[16], rtpNow);
+    SET_U32(&data[20], info.mNumPacketsSent);
+    SET_U32(&data[24], info.mNumBytesSent);
+}
+
+void RTPSender::appendSDES(std::vector<uint8_t> *buffer, uint32_t localSSRC) {
+    static constexpr uint8_t SDES = 202;
+
+    static const char *const kCNAME = "myWebRTP";
+    static const size_t kCNAMELength = strlen(kCNAME);
+
+    const size_t kLengthInWords = 2 + (2 + kCNAMELength + 1 + 3) / 4;
+
+    auto offset = buffer->size();
+    buffer->resize(offset + kLengthInWords * sizeof(uint32_t));
+
+    uint8_t *data = buffer->data() + offset;
+
+    data[0] = 0x81;
+    data[1] = SDES;
+    SET_U16(&data[2], kLengthInWords - 1);
+    SET_U32(&data[4], localSSRC);
+
+    data[8] = 1; // CNAME
+    data[9] = kCNAMELength;
+    memcpy(&data[10], kCNAME, kCNAMELength);
+    data[10 + kCNAMELength] = '\0';
+}
+
+void RTPSender::queueDLRR(
+        uint32_t localSSRC,
+        uint32_t remoteSSRC,
+        uint32_t ntpHi,
+        uint32_t ntpLo) {
+    std::vector<uint8_t> buffer;
+    appendDLRR(&buffer, localSSRC, remoteSSRC, ntpHi, ntpLo);
+
+    mParent->queueRTCPDatagram(buffer.data(), buffer.size());
+}
+
+void RTPSender::appendDLRR(
+        std::vector<uint8_t> *buffer,
+        uint32_t localSSRC,
+        uint32_t remoteSSRC,
+        uint32_t ntpHi,
+        uint32_t ntpLo) {
+    static constexpr uint8_t XR = 207;
+
+    static constexpr uint8_t FMT_DLRRRB = 5;
+
+    const size_t kLengthInWords = 2 + 4;
+
+    auto offset = buffer->size();
+    buffer->resize(offset + kLengthInWords * sizeof(uint32_t));
+
+    uint8_t *data = buffer->data() + offset;
+
+    data[0] = 0x80;
+    data[1] = XR;
+    SET_U16(&data[2], kLengthInWords - 1);
+    SET_U32(&data[4], localSSRC);
+
+    data[8] = FMT_DLRRRB;
+    data[9] = 0x00;
+    SET_U16(&data[10], 3 /* block length */);
+    SET_U32(&data[12], remoteSSRC);
+    SET_U32(&data[16], (ntpHi << 16) | (ntpLo >> 16));
+    SET_U32(&data[20], 0 /* delay since last RR */);
+}
+
+void RTPSender::queueSR(uint32_t localSSRC) {
+    std::vector<uint8_t> buffer;
+    appendSR(&buffer, localSSRC);
+    // appendSDES(&buffer, localSSRC);
+
+    // LOG(INFO) << "RTPSender::queueSR";
+    // android::hexdump(buffer.data(), buffer.size());
+
+    mParent->queueRTCPDatagram(buffer.data(), buffer.size());
+}
+
+void RTPSender::sendSR(uint32_t localSSRC) {
+    // LOG(INFO) << "sending SR.";
+    queueSR(localSSRC);
+
+    mRunLoop->postWithDelay(
+            std::chrono::seconds(1),
+            makeSafeCallback(this, &RTPSender::sendSR, localSSRC));
+}
+
+void RTPSender::run() {
+    for (const auto &entry : mSources) {
+        sendSR(entry.first);
+    }
+}
+
+void RTPSender::queueRTPDatagram(std::vector<uint8_t> *packet) {
+    CHECK_GE(packet->size(), 12u);
+
+    uint32_t SSRC = U32_AT(&packet->data()[8]);
+
+    auto it = mSources.find(SSRC);
+    CHECK(it != mSources.end());
+
+    auto &info = it->second;
+
+    uint16_t seqNum = info.mNumPacketsSent;
+    SET_U16(packet->data() + 2, seqNum);
+
+#if SIMULATE_PACKET_LOSS
+    static std::random_device rd;
+    static std::mt19937 gen(rd());
+    static std::uniform_real_distribution<> dist(0.0, 1.0);
+    if (dist(gen) < 0.99) {
+#endif
+        mParent->queueRTPDatagram(packet->data(), packet->size());
+#if SIMULATE_PACKET_LOSS
+    } else {
+        LOG(WARNING)
+            << "dropping packet "
+            << StringPrintf("0x%04x", seqNum)
+            << " from SSRC "
+            << StringPrintf("0x%08x", SSRC);
+    }
+#endif
+
+    ++info.mNumPacketsSent;
+    info.mNumBytesSent += packet->size() - 12;  // does not include RTP header.
+
+    if (!info.mRetrans.empty()) {
+        static constexpr size_t kMaxHistory = 512;
+        if (info.mRecentPackets.size() == kMaxHistory) {
+            info.mRecentPackets.pop_front();
+        }
+        // info.mRecentPackets.push_back(std::move(*packet));
+        info.mRecentPackets.push_back(*packet);
+    }
+}
+
+void RTPSender::retransmitPackets(
+        uint32_t localSSRC, uint16_t PID, uint16_t BLP) {
+    auto it = mSources.find(localSSRC);
+    CHECK(it != mSources.end());
+
+    const auto &info = it->second;
+
+    if (!info.mRecentPackets.empty()) {
+        LOG(INFO) << "Recent packets cover range ["
+            << StringPrintf(
+                    "0x%04x", U16_AT(info.mRecentPackets.front().data() + 2))
+            << ";"
+            << StringPrintf(
+                    "0x%04x", U16_AT(info.mRecentPackets.back().data() + 2))
+            << "]";
+    } else {
+        LOG(INFO) << "Recent packets are EMPTY!";
+    }
+
+    bool first = true;
+    while (first || BLP) {
+        if (first) {
+            first = false;
+        } else {
+            ++PID;
+            if (!(BLP & 1)) {
+                BLP = BLP >> 1;
+                continue;
+            }
+
+            BLP = BLP >> 1;
+        }
+
+        for (auto it = info.mRecentPackets.begin();
+                it != info.mRecentPackets.end();
+                ++it) {
+            const auto &origPacket = *it;
+            auto seqNum = U16_AT(origPacket.data() + 2);
+
+            if (seqNum != PID) {
+                continue;
+            }
+
+            LOG(INFO) << "Retransmitting PID " << StringPrintf("0x%04x", PID);
+
+            auto PT = origPacket[1] & 0x7f;
+            auto it2 = info.mRetrans.find(PT);
+            CHECK(it2 != info.mRetrans.end());
+
+            auto [rtxSSRC, rtxPT] = it2->second;
+
+            std::vector<uint8_t> packet(origPacket.size() + 2);
+
+            // XXX This is very simplified and assumes that the original packet
+            // started with a standard 12-byte header, no extensions and no padding!
+            memcpy(packet.data(), origPacket.data(), 12);
+
+            packet[1] = (origPacket[1] & 0x80) | (rtxPT & 0x7f);
+            SET_U32(packet.data() + 8, rtxSSRC);
+            SET_U16(packet.data() + 12, seqNum);
+
+            memcpy(packet.data() + 14,
+                   origPacket.data() + 12,
+                   origPacket.size() - 12);
+
+            // queueRTPDatagram will fill in the new seqNum.
+            queueRTPDatagram(&packet);
+        }
+    }
+}
+
+void RTPSender::requestIDRFrame() {
+    if (mVideoPacketizer) {
+        mVideoPacketizer->requestIDRFrame();
+    }
+}
+
diff --git a/host/frontend/gcastv2/webrtc/RTPSession.cpp b/host/frontend/gcastv2/webrtc/RTPSession.cpp
new file mode 100644
index 0000000..62e46fc
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/RTPSession.cpp
@@ -0,0 +1,126 @@
+#include <webrtc/RTPSession.h>
+
+#include <media/stagefright/foundation/ADebug.h>
+
+#include <sstream>
+
+RTPSession::RTPSession(
+        std::string_view localUFrag,
+        std::string_view localPassword,
+        std::shared_ptr<X509> localCertificate,
+        std::shared_ptr<EVP_PKEY> localKey)
+    : mLocalUFrag(localUFrag),
+      mLocalPassword(localPassword),
+      mLocalCertificate(localCertificate),
+      mLocalKey(localKey),
+      mPingToken(0),
+      mIsActive(false) {
+}
+
+bool RTPSession::isActive() const {
+    return mIsActive;
+}
+
+void RTPSession::setIsActive() {
+    mIsActive = true;
+}
+
+void RTPSession::schedulePing(
+        std::shared_ptr<RunLoop> runLoop,
+        RunLoop::AsyncFunction cb,
+        std::chrono::steady_clock::duration delay) {
+    CHECK_EQ(mPingToken, 0);
+
+    mPingToken = runLoop->postWithDelay(
+            delay,
+            [weak_this = std::weak_ptr<RTPSession>(shared_from_this()),
+             runLoop, cb]() {
+                auto me = weak_this.lock();
+                if (me) {
+                    me->mPingToken = 0;
+                    cb();
+                }
+            });
+}
+
+void RTPSession::setRemoteParams(
+        std::string_view remoteUFrag,
+        std::string_view remotePassword,
+        std::string_view remoteFingerprint) {
+    CHECK(!mRemoteUFrag && !mRemotePassword && !mRemoteFingerprint);
+
+    mRemoteUFrag = remoteUFrag;
+    mRemotePassword = remotePassword;
+    mRemoteFingerprint = remoteFingerprint;
+}
+
+std::string RTPSession::localUFrag() const {
+    return mLocalUFrag;
+}
+
+std::string RTPSession::localPassword() const {
+    return mLocalPassword;
+}
+
+std::shared_ptr<X509> RTPSession::localCertificate() const {
+    return mLocalCertificate;
+}
+
+std::shared_ptr<EVP_PKEY> RTPSession::localKey() const {
+    return mLocalKey;
+}
+
+std::string RTPSession::localFingerprint() const {
+    auto digest = EVP_sha256();
+
+    unsigned char md[EVP_MAX_MD_SIZE];
+    unsigned int n;
+    auto res = X509_digest(mLocalCertificate.get(), digest, md, &n);
+    CHECK_EQ(res, 1);
+
+    std::stringstream ss;
+    ss << "sha-256 ";
+    for (unsigned int i = 0; i < n; ++i) {
+        if (i > 0) {
+            ss << ":";
+        }
+
+        uint8_t byte = md[i];
+        uint8_t nibble = byte >> 4;
+        ss << (char)(nibble < 10 ? '0' + nibble : 'A' + nibble - 10);
+        nibble = byte & 0xf;
+        ss << (char)(nibble < 10 ? '0' + nibble : 'A' + nibble - 10);
+    }
+
+    return ss.str();
+}
+
+std::string RTPSession::remoteUFrag() const {
+    CHECK(mRemoteUFrag.has_value());
+    return *mRemoteUFrag;
+}
+
+std::string RTPSession::remotePassword() const {
+    CHECK(mRemotePassword.has_value());
+    return *mRemotePassword;
+}
+
+std::string RTPSession::remoteFingerprint() const {
+    CHECK(mRemoteFingerprint.has_value());
+    return *mRemoteFingerprint;
+}
+
+bool RTPSession::hasRemoteAddress() const {
+    return mRemoteAddr.has_value();
+}
+
+sockaddr_storage RTPSession::remoteAddress() const {
+    CHECK(hasRemoteAddress());
+    return *mRemoteAddr;
+}
+
+void RTPSession::setRemoteAddress(const sockaddr_storage &remoteAddr) {
+    CHECK(!hasRemoteAddress());
+    mRemoteAddr = remoteAddr;
+}
+
diff --git a/host/frontend/gcastv2/webrtc/RTPSocketHandler.cpp b/host/frontend/gcastv2/webrtc/RTPSocketHandler.cpp
new file mode 100644
index 0000000..42b761a
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/RTPSocketHandler.cpp
@@ -0,0 +1,666 @@
+#include <webrtc/RTPSocketHandler.h>
+
+#include <webrtc/MyWebSocketHandler.h>
+#include <webrtc/STUNMessage.h>
+
+#include <https/PlainSocket.h>
+#include <https/SafeCallbackable.h>
+#include <https/Support.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/Utils.h>
+
+#include <netdb.h>
+#include <netinet/in.h>
+
+#include <cstring>
+#include <iostream>
+#include <set>
+
+#if defined(TARGET_ANDROID)
+#include <gflags/gflags.h>
+
+DECLARE_string(public_ip);
+#endif
+
+#ifdef TARGET_ANDROID_DEVICE
+#include <ifaddrs.h>
+#endif
+
+static socklen_t getSockAddrLen(const sockaddr_storage &addr) {
+    switch (addr.ss_family) {
+        case AF_INET:
+            return sizeof(sockaddr_in);
+        case AF_INET6:
+            return sizeof(sockaddr_in6);
+        default:
+            CHECK(!"Should not be here.");
+            return 0;
+    }
+}
+
+RTPSocketHandler::RTPSocketHandler(
+        std::shared_ptr<RunLoop> runLoop,
+        std::shared_ptr<ServerState> serverState,
+        int domain,
+        uint16_t port,
+        uint32_t trackMask,
+        std::shared_ptr<RTPSession> session)
+    : mRunLoop(runLoop),
+      mServerState(serverState),
+      mLocalPort(port),
+      mTrackMask(trackMask),
+      mSession(session),
+      mSendPending(false),
+      mDTLSConnected(false) {
+    int sock = socket(domain, SOCK_DGRAM, 0);
+
+    makeFdNonblocking(sock);
+    mSocket = std::make_shared<PlainSocket>(mRunLoop, sock);
+
+    sockaddr_storage addr;
+
+    if (domain == PF_INET) {
+        sockaddr_in addrV4;
+        memset(addrV4.sin_zero, 0, sizeof(addrV4.sin_zero));
+        addrV4.sin_family = AF_INET;
+        addrV4.sin_port = htons(port);
+        addrV4.sin_addr.s_addr = INADDR_ANY;
+        memcpy(&addr, &addrV4, sizeof(addrV4));
+    } else {
+        CHECK_EQ(domain, PF_INET6);
+
+        sockaddr_in6 addrV6;
+        addrV6.sin6_family = AF_INET6;
+        addrV6.sin6_port = htons(port);
+        addrV6.sin6_addr = in6addr_any;
+        addrV6.sin6_scope_id = 0;
+        memcpy(&addr, &addrV6, sizeof(addrV6));
+    }
+
+    int res = bind(
+            sock,
+            reinterpret_cast<const sockaddr *>(&addr),
+            getSockAddrLen(addr));
+
+    CHECK(!res);
+
+    auto videoPacketizer =
+        (trackMask & TRACK_VIDEO)
+            ? mServerState->getVideoPacketizer() : nullptr;
+
+    auto audioPacketizer =
+        (trackMask & TRACK_AUDIO)
+            ? mServerState->getAudioPacketizer() : nullptr;
+
+    mRTPSender = std::make_shared<RTPSender>(
+            mRunLoop,
+            this,
+            videoPacketizer,
+            audioPacketizer);
+
+    if (trackMask & TRACK_VIDEO) {
+        mRTPSender->addSource(0xdeadbeef);
+        mRTPSender->addSource(0xcafeb0b0);
+
+        mRTPSender->addRetransInfo(0xdeadbeef, 96, 0xcafeb0b0, 97);
+
+        videoPacketizer->addSender(mRTPSender);
+    }
+
+    if (trackMask & TRACK_AUDIO) {
+        mRTPSender->addSource(0x8badf00d);
+
+        audioPacketizer->addSender(mRTPSender);
+    }
+}
+
+#ifdef TARGET_ANDROID_DEVICE
+static std::string getSockAddrHostName(const sockaddr *addr) {
+    char buffer[256];
+
+    switch (addr->sa_family) {
+        case AF_INET:
+        {
+            auto addrV4 = reinterpret_cast<const sockaddr_in *>(addr);
+
+            auto out = inet_ntop(
+                    AF_INET, &addrV4->sin_addr, buffer, sizeof(buffer));
+
+            CHECK(out);
+            break;
+        }
+
+        case AF_INET6:
+        {
+            auto addrV6 = reinterpret_cast<const sockaddr_in6 *>(addr);
+
+            auto out = inet_ntop(
+                    AF_INET6, &addrV6->sin6_addr, buffer, sizeof(buffer));
+
+            CHECK(out);
+            break;
+        }
+
+        default:
+            CHECK(!"Should not be here.");
+    }
+
+    return buffer;
+}
+
+static std::string getWifiInterfaceAddress(int family) {
+    ifaddrs *tmp;
+    if (getifaddrs(&tmp)) {
+        LOG(ERROR)
+            << "getifaddrs return error "
+            << errno
+            << " ("
+            << strerror(errno)
+            << ")";
+
+        return "127.0.0.1";
+    }
+
+    std::unique_ptr<ifaddrs, std::function<void(ifaddrs *)>> ifaces(
+            tmp, freeifaddrs);
+
+    for (tmp = ifaces.get(); tmp; tmp = tmp->ifa_next) {
+        if (strcmp(tmp->ifa_name, "wlan0")
+                || tmp->ifa_addr->sa_family != family) {
+            continue;
+        }
+
+        return getSockAddrHostName(tmp->ifa_addr);
+    }
+
+    LOG(WARNING) << "getWifiInterfaceAddress did not find a 'wlan0' interface.";
+    return "127.0.0.1";
+}
+#endif
+
+uint16_t RTPSocketHandler::getLocalPort() const {
+    return mLocalPort;
+}
+
+std::string RTPSocketHandler::getLocalUFrag() const {
+    return mSession->localUFrag();
+}
+
+std::string RTPSocketHandler::getLocalIPString() const {
+#if 0
+    sockaddr_storage addr;
+    socklen_t addrLen = sizeof(addr);
+
+    int res = getsockname(
+            mSocket->fd(), reinterpret_cast<sockaddr *>(&addr), &addrLen);
+
+    CHECK(!res);
+
+    char buffer[256];
+
+    switch (addr.ss_family) {
+        case AF_INET:
+        {
+            sockaddr_in addrV4;
+            memcpy(&addrV4, &addr, sizeof(addrV4));
+
+            auto out = inet_ntop(AF_INET, &addrV4.sin_addr, buffer, sizeof(buffer));
+            CHECK(out);
+
+            return "100.122.57.45";  // XXX
+            break;
+        }
+
+        case AF_INET6:
+        {
+            sockaddr_in6 addrV6;
+            memcpy(&addrV6, &addr, sizeof(addrV6));
+
+            auto out = inet_ntop(AF_INET6, &addrV6.sin6_addr, buffer, sizeof(buffer));
+            CHECK(out);
+
+            return "2620::1000:1610:b5d5:7493:a307:ca94";  // XXX
+            break;
+        }
+
+        default:
+            CHECK(!"Should not be here.");
+    }
+
+    return std::string(buffer);
+#elif defined(TARGET_ANDROID)
+    return FLAGS_public_ip;
+#elif defined(TARGET_ANDROID_DEVICE)
+    return getWifiInterfaceAddress(AF_INET);
+#else
+    return "127.0.0.1";
+#endif
+}
+
+void RTPSocketHandler::run() {
+    mSocket->postRecv(makeSafeCallback(this, &RTPSocketHandler::onReceive));
+}
+
+void RTPSocketHandler::onReceive() {
+    std::vector<uint8_t> buffer(kMaxUDPPayloadSize);
+
+    uint8_t *data = buffer.data();
+
+    sockaddr_storage addr;
+    socklen_t addrLen = sizeof(addr);
+
+    auto n = mSocket->recvfrom(
+            data, buffer.size(), reinterpret_cast<sockaddr *>(&addr), &addrLen);
+
+#if 0
+    std::cout << "========================================" << std::endl;
+
+    hexdump(data, n);
+#endif
+
+    STUNMessage msg(data, n);
+    if (!msg.isValid()) {
+        if (mDTLSConnected) {
+            int err = -EINVAL;
+            if (mRTPSender) {
+                err = onSRTPReceive(data, static_cast<size_t>(n));
+            }
+
+            if (err == -EINVAL) {
+                LOG(VERBOSE) << "Sending to DTLS instead:";
+                // hexdump(data, n);
+
+                onDTLSReceive(data, static_cast<size_t>(n));
+
+                if (mTrackMask & TRACK_DATA) {
+                    ssize_t n;
+
+                    do {
+                        uint8_t buf[kMaxUDPPayloadSize];
+                        n = mDTLS->readApplicationData(buf, sizeof(buf));
+
+                        if (n > 0) {
+                            auto err = mSCTPHandler->inject(
+                                    buf, static_cast<size_t>(n));
+
+                            if (err) {
+                                LOG(WARNING)
+                                    << "SCTPHandler::inject returned error "
+                                    << err;
+                            }
+                        }
+                    } while (n > 0);
+                }
+            }
+        } else {
+            onDTLSReceive(data, static_cast<size_t>(n));
+        }
+
+        run();
+        return;
+    }
+
+    if (msg.type() == 0x0001 /* Binding Request */) {
+        STUNMessage response(0x0101 /* Binding Response */, msg.data() + 8);
+
+        if (!matchesSession(msg)) {
+            LOG(WARNING) << "Unknown session or no USERNAME.";
+            run();
+            return;
+        }
+
+        const auto &answerPassword = mSession->localPassword();
+
+        // msg.dump(answerPassword);
+
+        if (addr.ss_family == AF_INET) {
+            uint8_t attr[8];
+            attr[0] = 0x00;
+
+            sockaddr_in addrV4;
+            CHECK_EQ(addrLen, sizeof(addrV4));
+
+            memcpy(&addrV4, &addr, addrLen);
+
+            attr[1] = 0x01;  // IPv4
+
+            static constexpr uint32_t kMagicCookie = 0x2112a442;
+
+            uint16_t portHost = ntohs(addrV4.sin_port);
+            portHost ^= (kMagicCookie >> 16);
+
+            uint32_t ipHost = ntohl(addrV4.sin_addr.s_addr);
+            ipHost ^= kMagicCookie;
+
+            attr[2] = portHost >> 8;
+            attr[3] = portHost & 0xff;
+            attr[4] = ipHost >> 24;
+            attr[5] = (ipHost >> 16) & 0xff;
+            attr[6] = (ipHost >> 8) & 0xff;
+            attr[7] = ipHost & 0xff;
+
+            response.addAttribute(
+                    0x0020 /* XOR-MAPPED-ADDRESS */, attr, sizeof(attr));
+        } else {
+            uint8_t attr[20];
+            attr[0] = 0x00;
+
+            CHECK_EQ(addr.ss_family, AF_INET6);
+
+            sockaddr_in6 addrV6;
+            CHECK_EQ(addrLen, sizeof(addrV6));
+
+            memcpy(&addrV6, &addr, addrLen);
+
+            attr[1] = 0x02;  // IPv6
+
+            static constexpr uint32_t kMagicCookie = 0x2112a442;
+
+            uint16_t portHost = ntohs(addrV6.sin6_port);
+            portHost ^= (kMagicCookie >> 16);
+
+            attr[2] = portHost >> 8;
+            attr[3] = portHost & 0xff;
+
+            uint8_t ipHost[16];
+
+            std::string out;
+
+            for (size_t i = 0; i < 16; ++i) {
+                ipHost[i] = addrV6.sin6_addr.s6_addr[15 - i];
+
+                if (!out.empty()) {
+                    out += ":";
+                }
+                out += android::StringPrintf("%02x", ipHost[i]);
+
+                ipHost[i] ^= response.data()[4 + i];
+            }
+
+            // LOG(INFO) << "IP6 = " << out;
+
+            for (size_t i = 0; i < 16; ++i) {
+                attr[4 + i] = ipHost[15 - i];
+            }
+
+            response.addAttribute(
+                    0x0020 /* XOR-MAPPED-ADDRESS */, attr, sizeof(attr));
+        }
+
+        response.addMessageIntegrityAttribute(answerPassword);
+        response.addFingerprint();
+
+        // response.dump(answerPassword);
+
+        auto res =
+            mSocket->sendto(
+                    response.data(),
+                    response.size(),
+                    reinterpret_cast<const sockaddr *>(&addr),
+                    addrLen);
+
+        CHECK_GT(res, 0);
+        CHECK_EQ(static_cast<size_t>(res), response.size());
+
+        if (!mSession->isActive()) {
+            mSession->setRemoteAddress(addr);
+
+            mSession->setIsActive();
+
+            mSession->schedulePing(
+                    mRunLoop,
+                    makeSafeCallback(
+                        this, &RTPSocketHandler::pingRemote, mSession),
+                    std::chrono::seconds(0));
+        }
+
+    } else {
+        // msg.dump();
+
+        if (msg.type() == 0x0101 && !mDTLS) {
+            mDTLS = std::make_shared<DTLS>(
+                    shared_from_this(),
+                    DTLS::Mode::ACCEPT,
+                    mSession->localCertificate(),
+                    mSession->localKey(),
+                    mSession->remoteFingerprint(),
+                    (mTrackMask != TRACK_DATA) /* useSRTP */);
+
+            mDTLS->connect(mSession->remoteAddress());
+        }
+    }
+
+    run();
+}
+
+bool RTPSocketHandler::matchesSession(const STUNMessage &msg) const {
+    const void *attrData;
+    size_t attrSize;
+    if (!msg.findAttribute(0x0006 /* USERNAME */, &attrData, &attrSize)) {
+        return false;
+    }
+
+    std::string uFragPair(static_cast<const char *>(attrData), attrSize);
+    auto colonPos = uFragPair.find(':');
+
+    if (colonPos == std::string::npos) {
+        return false;
+    }
+
+    std::string localUFrag(uFragPair, 0, colonPos);
+    std::string remoteUFrag(uFragPair, colonPos + 1);
+
+    if (mSession->localUFrag() != localUFrag
+            || mSession->remoteUFrag() != remoteUFrag) {
+
+        LOG(WARNING)
+            << "Unable to find session localUFrag='"
+            << localUFrag
+            << "', remoteUFrag='"
+            << remoteUFrag
+            << "'";
+
+        return false;
+    }
+
+    return true;
+}
+
+void RTPSocketHandler::pingRemote(std::shared_ptr<RTPSession> session) {
+    std::vector<uint8_t> transactionID { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
+
+    STUNMessage msg(
+            0x0001 /* Binding Request */,
+            transactionID.data());
+
+    std::string uFragPair =
+            session->remoteUFrag() + ":" + session->localUFrag();
+
+    msg.addAttribute(
+            0x0006 /* USERNAME */,
+            uFragPair.c_str(),
+            uFragPair.size());
+
+    uint64_t tieBreaker = 0xdeadbeefcafeb0b0;  // XXX
+    msg.addAttribute(
+            0x802a /* ICE-CONTROLLING */,
+            &tieBreaker,
+            sizeof(tieBreaker));
+
+    uint32_t priority = 0xdeadbeef;
+    msg.addAttribute(
+            0x0024 /* PRIORITY */, &priority, sizeof(priority));
+
+    // We're the controlling agent and including the "USE-CANDIDATE" attribute
+    // below nominates this candidate.
+    msg.addAttribute(0x0025 /* USE_CANDIDATE */);
+
+    msg.addMessageIntegrityAttribute(session->remotePassword());
+    msg.addFingerprint();
+
+    queueDatagram(session->remoteAddress(), msg.data(), msg.size());
+
+    session->schedulePing(
+            mRunLoop,
+            makeSafeCallback(this, &RTPSocketHandler::pingRemote, session),
+            std::chrono::seconds(1));
+}
+
+RTPSocketHandler::Datagram::Datagram(
+        const sockaddr_storage &addr, const void *data, size_t size)
+    : mData(size),
+      mAddr(addr) {
+    memcpy(mData.data(), data, size);
+}
+
+const void *RTPSocketHandler::Datagram::data() const {
+    return mData.data();
+}
+
+size_t RTPSocketHandler::Datagram::size() const {
+    return mData.size();
+}
+
+const sockaddr_storage &RTPSocketHandler::Datagram::remoteAddress() const {
+    return mAddr;
+}
+
+void RTPSocketHandler::queueDatagram(
+        const sockaddr_storage &addr, const void *data, size_t size) {
+    auto datagram = std::make_shared<Datagram>(addr, data, size);
+
+    CHECK_LE(size, RTPSocketHandler::kMaxUDPPayloadSize);
+
+    mRunLoop->post(
+            makeSafeCallback<RTPSocketHandler>(
+                this,
+                [datagram](RTPSocketHandler *me) {
+                    me->mOutQueue.push_back(datagram);
+
+                    if (!me->mSendPending) {
+                        me->scheduleDrainOutQueue();
+                    }
+                }));
+}
+
+void RTPSocketHandler::scheduleDrainOutQueue() {
+    CHECK(!mSendPending);
+
+    mSendPending = true;
+    mSocket->postSend(
+            makeSafeCallback(
+                this, &RTPSocketHandler::drainOutQueue));
+}
+
+void RTPSocketHandler::drainOutQueue() {
+    mSendPending = false;
+
+    CHECK(!mOutQueue.empty());
+
+    do {
+        auto datagram = mOutQueue.front();
+
+        ssize_t n;
+        do {
+            const sockaddr_storage &remoteAddr = datagram->remoteAddress();
+
+            n = mSocket->sendto(
+                    datagram->data(),
+                    datagram->size(),
+                    reinterpret_cast<const sockaddr *>(&remoteAddr),
+                    getSockAddrLen(remoteAddr));
+        } while (n < 0 && errno == EINTR);
+
+        if (n < 0) {
+            if (errno == EAGAIN || errno == EWOULDBLOCK) {
+                break;
+            }
+
+            CHECK(!"Should not be here");
+        }
+
+        mOutQueue.pop_front();
+
+    } while (!mOutQueue.empty());
+
+    if (!mOutQueue.empty()) {
+        scheduleDrainOutQueue();
+    }
+}
+
+void RTPSocketHandler::onDTLSReceive(const uint8_t *data, size_t size) {
+    if (mDTLS) {
+        mDTLS->inject(data, size);
+    }
+}
+
+void RTPSocketHandler::notifyDTLSConnected() {
+    LOG(INFO) << "TDLS says that it's now connected.";
+
+    mDTLSConnected = true;
+
+    if (mTrackMask & TRACK_DATA) {
+        mSCTPHandler = std::make_shared<SCTPHandler>(mRunLoop, mDTLS);
+        mSCTPHandler->run();
+    }
+
+    mRTPSender->run();
+}
+
+int RTPSocketHandler::onSRTPReceive(uint8_t *data, size_t size) {
+#if 0
+    LOG(INFO) << "onSRTPReceive";
+    hexdump(data, size);
+#endif
+
+    if (size < 2) {
+        return -EINVAL;
+    }
+
+    auto version = data[0] >> 6;
+    if (version != 2) {
+        return -EINVAL;
+    }
+
+    auto outSize = mDTLS->unprotect(data, size, false /* isRTP */);
+
+#if 0
+    LOG(INFO) << "After srtp_unprotect_rtcp" << ":";
+    hexdump(data, outSize);
+#endif
+
+    auto err = mRTPSender->injectRTCP(data, outSize);
+    if (err) {
+        LOG(WARNING) << "RTPSender::injectRTCP returned " << err;
+    }
+
+    return err;
+}
+
+void RTPSocketHandler::queueRTCPDatagram(const void *data, size_t size) {
+    if (!mDTLSConnected) {
+        return;
+    }
+
+    std::vector<uint8_t> copy(size + SRTP_MAX_TRAILER_LEN);
+    memcpy(copy.data(), data, size);
+
+    auto outSize = mDTLS->protect(copy.data(), size, false /* isRTP */);
+    CHECK_LE(outSize, copy.size());
+
+    queueDatagram(mSession->remoteAddress(), copy.data(), outSize);
+}
+
+void RTPSocketHandler::queueRTPDatagram(const void *data, size_t size) {
+    if (!mDTLSConnected) {
+        return;
+    }
+
+    std::vector<uint8_t> copy(size + SRTP_MAX_TRAILER_LEN);
+    memcpy(copy.data(), data, size);
+
+    auto outSize = mDTLS->protect(copy.data(), size, true /* isRTP */);
+    CHECK_LE(outSize, copy.size());
+
+    queueDatagram(mSession->remoteAddress(), copy.data(), outSize);
+}
diff --git a/host/frontend/gcastv2/webrtc/SCTPHandler.cpp b/host/frontend/gcastv2/webrtc/SCTPHandler.cpp
new file mode 100644
index 0000000..ffe1a0c
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/SCTPHandler.cpp
@@ -0,0 +1,516 @@
+#include <webrtc/SCTPHandler.h>
+
+#include "Utils.h"
+
+#include <https/SafeCallbackable.h>
+#include <https/Support.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/Utils.h>
+
+using android::U16_AT;
+using android::U32_AT;
+
+SCTPHandler::SCTPHandler(
+        std::shared_ptr<RunLoop> runLoop,
+        std::shared_ptr<DTLS> dtls)
+    : mRunLoop(runLoop),
+      mDTLS(dtls),
+      mInitiateTag(0),
+      mSendingTSN(0),
+      mSentGreeting(false) {
+}
+
+void SCTPHandler::run() {
+}
+
+int SCTPHandler::inject(uint8_t *data, size_t size) {
+    LOG(INFO)
+        << "Received SCTP datagram of size " << size << ":";
+
+    hexdump(data, size);
+
+    if (size < 12) {
+        // Need at least the common header.
+        return -EINVAL;
+    }
+
+    auto srcPort = U16_AT(&data[0]);
+    auto dstPort = U16_AT(&data[2]);
+
+    if (dstPort != 5000) {
+        return -EINVAL;
+    }
+
+    auto checkSumIn = U32_AT(&data[8]);
+    SET_U32(&data[8], 0x00000000);
+    auto checkSum = crc32c(data, size);
+
+    if (checkSumIn != checkSum) {
+        LOG(WARNING)
+            << "SCTPHandler::inject checksum invalid."
+            << " (in: " << android::StringPrintf("0x%08x", checkSumIn) << ", "
+            << "computed: " << android::StringPrintf("0x%08x", checkSum) << ")";
+
+        return -EINVAL;
+    }
+
+    bool firstChunk = true;
+    size_t offset = 12;
+    while (offset < size) {
+        if (offset + 4 > size) {
+            return -EINVAL;
+        }
+
+        size_t chunkLength = U16_AT(&data[offset + 2]);
+
+        if (offset + chunkLength > size) {
+            return -EINVAL;
+        }
+
+        size_t paddedChunkLength = chunkLength;
+        size_t pad = chunkLength % 4;
+        if (pad) {
+            pad = 4 - pad;
+            paddedChunkLength += pad;
+        }
+
+        bool lastChunk =
+            (offset + chunkLength == size)
+                || (offset + paddedChunkLength == size);
+
+        auto err = processChunk(
+                srcPort,
+                &data[offset],
+                chunkLength,
+                firstChunk,
+                lastChunk);
+
+        if (err) {
+            return err;
+        }
+
+        firstChunk = false;
+
+        offset += chunkLength;
+
+        if (offset == size) {
+            break;
+        }
+
+        if (offset + pad > size) {
+            return -EINVAL;
+        }
+
+        offset += pad;
+    }
+
+    return 0;
+}
+
+int SCTPHandler::processChunk(
+        uint16_t srcPort,
+        const uint8_t *data,
+        size_t size,
+        bool firstChunk,
+        bool lastChunk) {
+    static constexpr uint8_t DATA = 0;
+    static constexpr uint8_t INIT = 1;
+    static constexpr uint8_t INIT_ACK = 2;
+    static constexpr uint8_t SACK = 3;
+    static constexpr uint8_t HEARTBEAT = 4;
+    static constexpr uint8_t HEARTBEAT_ACK = 5;
+    static constexpr uint8_t COOKIE_ECHO = 10;
+    static constexpr uint8_t COOKIE_ACK = 11;
+    static constexpr uint8_t SHUTDOWN_COMPLETE = 14;
+
+    static constexpr uint64_t kCookie = 0xDABBAD00DEADBAADull;
+
+    auto chunkType = data[0];
+    if ((!firstChunk || !lastChunk)
+            && (chunkType == INIT
+                    || chunkType == INIT_ACK
+                    || chunkType == SHUTDOWN_COMPLETE)) {
+        // These chunks must be by themselves, no other chunks must be part
+        // of the same datagram.
+
+        return -EINVAL;
+    }
+
+    switch (chunkType) {
+        case INIT:
+        {
+            if (size < 20) {
+                return -EINVAL;
+            }
+
+            mInitiateTag = U32_AT(&data[4]);
+
+            uint8_t out[12 + 24 + sizeof(kCookie)];
+            SET_U16(&out[0], 5000);
+            SET_U16(&out[2], srcPort);
+            SET_U32(&out[4], mInitiateTag);
+            SET_U32(&out[8], 0x00000000);  // Checksum: to be filled in below.
+
+            size_t offset = 12;
+            out[offset++] = INIT_ACK;
+            out[offset++] = 0x00;
+
+            SET_U16(&out[offset], sizeof(out) - 12);
+            offset += 2;
+
+            SET_U32(&out[offset], 0xb0b0cafe);  // initiate tag
+            offset += 4;
+
+            SET_U32(&out[offset], 0x00020000);  // a_rwnd
+            offset += 4;
+
+            SET_U16(&out[offset], 1);  // Number of Outbound Streams
+            offset += 2;
+
+            SET_U16(&out[offset], 1);  // Number of Inbound Streams
+            offset += 2;
+
+            mSendingTSN = 0x12345678;
+
+            SET_U32(&out[offset], mSendingTSN);  // Initial TSN
+            offset += 4;
+
+            SET_U16(&out[offset], 0x0007);  // STATE_COOKIE
+            offset += 2;
+
+            static_assert((sizeof(kCookie) % 4) == 0);
+
+            SET_U16(&out[offset], 4 + sizeof(kCookie));
+            offset += 2;
+
+            memcpy(&out[offset], &kCookie, sizeof(kCookie));
+            offset += sizeof(kCookie);
+
+            CHECK_EQ(offset, sizeof(out));
+
+            SET_U32(&out[8], crc32c(out, sizeof(out)));
+
+            LOG(INFO) << "Sending SCTP INIT_ACK:";
+            hexdump(out, sizeof(out));
+
+            mDTLS->writeApplicationData(out, sizeof(out));
+            break;
+        }
+
+        case COOKIE_ECHO:
+        {
+            if (size != (4 + sizeof(kCookie))) {
+                return -EINVAL;
+            }
+
+            if (memcmp(&data[4], &kCookie, sizeof(kCookie))) {
+                return -EINVAL;
+            }
+
+            uint8_t out[12 + 4];
+            SET_U16(&out[0], 5000);
+            SET_U16(&out[2], srcPort);
+            SET_U32(&out[4], mInitiateTag);
+            SET_U32(&out[8], 0x00000000);  // Checksum: to be filled in below.
+
+            size_t offset = 12;
+            out[offset++] = COOKIE_ACK;
+            out[offset++] = 0x00;
+            SET_U16(&out[offset], sizeof(out) - 12);
+            offset += 2;
+
+            CHECK_EQ(offset, sizeof(out));
+
+            SET_U32(&out[8], crc32c(out, sizeof(out)));
+
+            LOG(INFO) << "Sending SCTP COOKIE_ACK:";
+            hexdump(out, sizeof(out));
+
+            mDTLS->writeApplicationData(out, sizeof(out));
+            break;
+        }
+
+        case DATA:
+        {
+            if (size < 17) {
+                // Minimal size (16 bytes header + 1 byte payload), empty
+                // payloads are prohibited.
+                return -EINVAL;
+            }
+
+            auto TSN = U32_AT(&data[4]);
+
+            uint8_t out[12 + 16];
+            SET_U16(&out[0], 5000);
+            SET_U16(&out[2], srcPort);
+            SET_U32(&out[4], mInitiateTag);
+            SET_U32(&out[8], 0x00000000);  // Checksum: to be filled in below.
+
+            size_t offset = 12;
+            out[offset++] = SACK;
+            out[offset++] = 0x00;
+
+            SET_U16(&out[offset], sizeof(out) - 12);
+            offset += 2;
+
+            SET_U32(&out[offset], TSN);
+            offset += 4;
+
+            SET_U32(&out[offset], 0x00020000);  // a_rwnd
+            offset += 4;
+
+            SET_U16(&out[offset], 0);  // Number of Gap Ack Blocks
+            offset += 2;
+
+            SET_U16(&out[offset], 0);  // Number of Duplicate TSNs
+            offset += 2;
+
+            CHECK_EQ(offset, sizeof(out));
+
+            SET_U32(&out[8], crc32c(out, sizeof(out)));
+
+            LOG(INFO) << "Sending SCTP SACK:";
+            hexdump(out, sizeof(out));
+
+            mDTLS->writeApplicationData(out, sizeof(out));
+
+            if (!mSentGreeting) {
+                mRunLoop->postWithDelay(
+                        std::chrono::seconds(1),
+                        makeSafeCallback(
+                            this,
+                            &SCTPHandler::onSendGreeting,
+                            srcPort,
+                            (size_t)0 /* index */));
+
+                mSentGreeting = true;
+            }
+            break;
+        }
+
+        case HEARTBEAT:
+        {
+            if (size < 8) {
+                return -EINVAL;
+            }
+
+            if (U16_AT(&data[4]) != 1 /* Heartbeat Info Type */
+                || size != (U16_AT(&data[6]) + 4)) {
+                return -EINVAL;
+            }
+
+            size_t pad = size % 4;
+            if (pad) {
+                pad = 4 - pad;
+            }
+
+            std::vector<uint8_t> outVec(12 + size + pad);
+
+            uint8_t *out = outVec.data();
+            SET_U16(&out[0], 5000);
+            SET_U16(&out[2], srcPort);
+            SET_U32(&out[4], mInitiateTag);
+            SET_U32(&out[8], 0x00000000);  // Checksum: to be filled in below.
+
+            size_t offset = 12;
+            out[offset++] = HEARTBEAT_ACK;
+            out[offset++] = 0x00;
+
+            SET_U16(&out[offset], outVec.size() - 12 - pad);
+            offset += 2;
+
+            memcpy(&out[offset], &data[4], size - 4);
+            offset += size - 4;
+
+            memset(&out[offset], 0x00, pad);
+            offset += pad;
+
+            CHECK_EQ(offset, outVec.size());
+
+            SET_U32(&out[8], crc32c(out, outVec.size()));
+
+            LOG(INFO) << "Sending SCTP HEARTBEAT_ACK:";
+            hexdump(out, outVec.size());
+
+            mDTLS->writeApplicationData(out, outVec.size());
+            break;
+        }
+
+        default:
+            break;
+    }
+
+    return 0;
+}
+
+void SCTPHandler::onSendGreeting(uint16_t srcPort, size_t index) {
+    static constexpr uint8_t DATA = 0;
+    // static constexpr uint8_t PPID_WEBRTC_CONTROL = 0x32;
+    static constexpr uint8_t PPID_WEBRTC_STRING  = 0x33;
+
+    std::string message;
+    if (index == 0) {
+        message = "Howdy! How's y'all doin?";
+    } else {
+        message = "But wait... There's more!";
+    }
+
+    size_t pad = message.size() % 4;
+    if (pad) {
+        pad = 4 - pad;
+    }
+
+    std::vector<uint8_t> outVec(12 + 16 + message.size() + pad);
+
+    uint8_t *out = outVec.data();
+    SET_U16(&out[0], 5000);
+    SET_U16(&out[2], srcPort);
+    SET_U32(&out[4], mInitiateTag);
+    SET_U32(&out[8], 0x00000000);  // Checksum: to be filled in below.
+
+    size_t offset = 12;
+    out[offset++] = DATA;
+    out[offset++] = 0x03;  // both Beginning and End of user message.
+
+    SET_U16(&out[offset], outVec.size() - 12 - pad);
+    offset += 2;
+
+    SET_U32(&out[offset], mSendingTSN);  // TSN
+    offset += 4;
+
+    ++mSendingTSN;
+
+    SET_U16(&out[offset], 0);  // Stream Identifier
+    offset += 2;
+
+    SET_U16(&out[offset], index);  // Stream Sequence Number
+    offset += 2;
+
+    SET_U32(&out[offset], PPID_WEBRTC_STRING);  // Payload Protocol Identifier
+    offset += 4;
+
+    // https://tools.ietf.org/html/draft-ietf-rtcweb-data-protocol-08#section-5.1
+    // https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-11#section-6.5
+
+    // DATA(payload protocol=0x32 (50, WebRTC Control), sequence 0)
+    // 03 00 00 00 00 00 00 00  ........
+    // 00 0c 00 00 64 61 74 61  ....data
+    // 2d 63 68 61 6e 6e 65 6c  -channel
+
+    // DATA(payload protocol=0x33 (51, WebRTC String), sequence 1)
+    // "Hello, world!"
+
+    memcpy(&out[offset], message.data(), message.size());
+    offset += message.size();
+
+    memset(&out[offset], 0x00, pad);
+    offset += pad;
+
+    CHECK_EQ(offset, outVec.size());
+
+    SET_U32(&out[8], crc32c(out, outVec.size()));
+
+    LOG(INFO) << "Sending SCTP DATA:";
+    hexdump(out, outVec.size());
+
+    mDTLS->writeApplicationData(out, outVec.size());
+
+    if (index == 0) {
+        mRunLoop->postWithDelay(
+                std::chrono::seconds(3),
+                makeSafeCallback(
+                    this,
+                    &SCTPHandler::onSendGreeting,
+                    srcPort,
+                    (size_t)1 /* index */));
+    }
+}
+
+static const uint32_t crc_c[256] = {
+    0x00000000, 0xF26B8303, 0xE13B70F7, 0x1350F3F4,
+    0xC79A971F, 0x35F1141C, 0x26A1E7E8, 0xD4CA64EB,
+    0x8AD958CF, 0x78B2DBCC, 0x6BE22838, 0x9989AB3B,
+    0x4D43CFD0, 0xBF284CD3, 0xAC78BF27, 0x5E133C24,
+    0x105EC76F, 0xE235446C, 0xF165B798, 0x030E349B,
+    0xD7C45070, 0x25AFD373, 0x36FF2087, 0xC494A384,
+    0x9A879FA0, 0x68EC1CA3, 0x7BBCEF57, 0x89D76C54,
+    0x5D1D08BF, 0xAF768BBC, 0xBC267848, 0x4E4DFB4B,
+    0x20BD8EDE, 0xD2D60DDD, 0xC186FE29, 0x33ED7D2A,
+    0xE72719C1, 0x154C9AC2, 0x061C6936, 0xF477EA35,
+    0xAA64D611, 0x580F5512, 0x4B5FA6E6, 0xB93425E5,
+    0x6DFE410E, 0x9F95C20D, 0x8CC531F9, 0x7EAEB2FA,
+    0x30E349B1, 0xC288CAB2, 0xD1D83946, 0x23B3BA45,
+    0xF779DEAE, 0x05125DAD, 0x1642AE59, 0xE4292D5A,
+    0xBA3A117E, 0x4851927D, 0x5B016189, 0xA96AE28A,
+    0x7DA08661, 0x8FCB0562, 0x9C9BF696, 0x6EF07595,
+    0x417B1DBC, 0xB3109EBF, 0xA0406D4B, 0x522BEE48,
+    0x86E18AA3, 0x748A09A0, 0x67DAFA54, 0x95B17957,
+    0xCBA24573, 0x39C9C670, 0x2A993584, 0xD8F2B687,
+    0x0C38D26C, 0xFE53516F, 0xED03A29B, 0x1F682198,
+    0x5125DAD3, 0xA34E59D0, 0xB01EAA24, 0x42752927,
+    0x96BF4DCC, 0x64D4CECF, 0x77843D3B, 0x85EFBE38,
+    0xDBFC821C, 0x2997011F, 0x3AC7F2EB, 0xC8AC71E8,
+    0x1C661503, 0xEE0D9600, 0xFD5D65F4, 0x0F36E6F7,
+    0x61C69362, 0x93AD1061, 0x80FDE395, 0x72966096,
+    0xA65C047D, 0x5437877E, 0x4767748A, 0xB50CF789,
+    0xEB1FCBAD, 0x197448AE, 0x0A24BB5A, 0xF84F3859,
+    0x2C855CB2, 0xDEEEDFB1, 0xCDBE2C45, 0x3FD5AF46,
+    0x7198540D, 0x83F3D70E, 0x90A324FA, 0x62C8A7F9,
+    0xB602C312, 0x44694011, 0x5739B3E5, 0xA55230E6,
+    0xFB410CC2, 0x092A8FC1, 0x1A7A7C35, 0xE811FF36,
+    0x3CDB9BDD, 0xCEB018DE, 0xDDE0EB2A, 0x2F8B6829,
+    0x82F63B78, 0x709DB87B, 0x63CD4B8F, 0x91A6C88C,
+    0x456CAC67, 0xB7072F64, 0xA457DC90, 0x563C5F93,
+    0x082F63B7, 0xFA44E0B4, 0xE9141340, 0x1B7F9043,
+    0xCFB5F4A8, 0x3DDE77AB, 0x2E8E845F, 0xDCE5075C,
+    0x92A8FC17, 0x60C37F14, 0x73938CE0, 0x81F80FE3,
+    0x55326B08, 0xA759E80B, 0xB4091BFF, 0x466298FC,
+    0x1871A4D8, 0xEA1A27DB, 0xF94AD42F, 0x0B21572C,
+    0xDFEB33C7, 0x2D80B0C4, 0x3ED04330, 0xCCBBC033,
+    0xA24BB5A6, 0x502036A5, 0x4370C551, 0xB11B4652,
+    0x65D122B9, 0x97BAA1BA, 0x84EA524E, 0x7681D14D,
+    0x2892ED69, 0xDAF96E6A, 0xC9A99D9E, 0x3BC21E9D,
+    0xEF087A76, 0x1D63F975, 0x0E330A81, 0xFC588982,
+    0xB21572C9, 0x407EF1CA, 0x532E023E, 0xA145813D,
+    0x758FE5D6, 0x87E466D5, 0x94B49521, 0x66DF1622,
+    0x38CC2A06, 0xCAA7A905, 0xD9F75AF1, 0x2B9CD9F2,
+    0xFF56BD19, 0x0D3D3E1A, 0x1E6DCDEE, 0xEC064EED,
+    0xC38D26C4, 0x31E6A5C7, 0x22B65633, 0xD0DDD530,
+    0x0417B1DB, 0xF67C32D8, 0xE52CC12C, 0x1747422F,
+    0x49547E0B, 0xBB3FFD08, 0xA86F0EFC, 0x5A048DFF,
+    0x8ECEE914, 0x7CA56A17, 0x6FF599E3, 0x9D9E1AE0,
+    0xD3D3E1AB, 0x21B862A8, 0x32E8915C, 0xC083125F,
+    0x144976B4, 0xE622F5B7, 0xF5720643, 0x07198540,
+    0x590AB964, 0xAB613A67, 0xB831C993, 0x4A5A4A90,
+    0x9E902E7B, 0x6CFBAD78, 0x7FAB5E8C, 0x8DC0DD8F,
+    0xE330A81A, 0x115B2B19, 0x020BD8ED, 0xF0605BEE,
+    0x24AA3F05, 0xD6C1BC06, 0xC5914FF2, 0x37FACCF1,
+    0x69E9F0D5, 0x9B8273D6, 0x88D28022, 0x7AB90321,
+    0xAE7367CA, 0x5C18E4C9, 0x4F48173D, 0xBD23943E,
+    0xF36E6F75, 0x0105EC76, 0x12551F82, 0xE03E9C81,
+    0x34F4F86A, 0xC69F7B69, 0xD5CF889D, 0x27A40B9E,
+    0x79B737BA, 0x8BDCB4B9, 0x988C474D, 0x6AE7C44E,
+    0xBE2DA0A5, 0x4C4623A6, 0x5F16D052, 0xAD7D5351,
+};
+
+#define CRC32C_POLY 0x1EDC6F41
+#define CRC32C(c,d) (c=(c>>8)^crc_c[(c^(d))&0xFF])
+
+static uint32_t swap32(uint32_t x) {
+    return (x >> 24)
+        | (((x >> 16) & 0xff) << 8)
+        | (((x >> 8) & 0xff) << 16)
+        | ((x & 0xff) << 24);
+}
+
+// static
+uint32_t SCTPHandler::crc32c(const uint8_t *data, size_t size) {
+    uint32_t crc32 = ~(uint32_t)0;
+
+    for (size_t i = 0; i < size; ++i) {
+        CRC32C(crc32, data[i]);
+    }
+
+    return ~swap32(crc32);
+}
+
diff --git a/host/frontend/gcastv2/webrtc/SDP.cpp b/host/frontend/gcastv2/webrtc/SDP.cpp
new file mode 100644
index 0000000..341ff97
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/SDP.cpp
@@ -0,0 +1,163 @@
+#include <webrtc/SDP.h>
+
+#include "Utils.h"
+
+#include <media/stagefright/foundation/ADebug.h>
+
+#include <cerrno>
+#include <iostream>
+
+SDP::SDP()
+    : mInitCheck(-ENODEV),
+      mNewSectionEditorActive(false) {
+}
+
+int SDP::initCheck() const {
+    return mInitCheck;
+}
+
+size_t SDP::countSections() const {
+    CHECK(!mInitCheck);
+    return mLineIndexBySection.size();
+}
+
+void SDP::clear() {
+    mInitCheck = -ENODEV;
+    mLines.clear();
+    mLineIndexBySection.clear();
+}
+
+int SDP::setTo(const std::string &data) {
+    clear();
+
+    mLines = SplitString(data, "\r\n");
+
+    LOG(VERBOSE) << "SDP contained " << mLines.size() << " lines.";
+
+    mLineIndexBySection.push_back(0);
+
+    mInitCheck = 0;
+
+    for (size_t i = 0; i < mLines.size(); ++i) {
+        const auto &line = mLines[i];
+
+        LOG(VERBOSE) << "Line #" << i << ": " << line;
+
+        if (i == 0 && line != "v=0") {
+            mInitCheck = -EINVAL;
+            break;
+        }
+
+        if (line.size() < 2 || line[1] != '=') {
+            mInitCheck = -EINVAL;
+            break;
+        }
+
+        if (line[0] == 'm') {
+            mLineIndexBySection.push_back(i);
+        }
+    }
+
+    return mInitCheck;
+}
+
+void SDP::getSectionRange(
+        size_t section, size_t *lineStartIndex, size_t *lineStopIndex) const {
+    CHECK(!mInitCheck);
+    CHECK_LT(section, mLineIndexBySection.size());
+
+    if (lineStartIndex) {
+        *lineStartIndex = mLineIndexBySection[section];
+    }
+
+    if (lineStopIndex) {
+        if (section + 1 < mLineIndexBySection.size()) {
+            *lineStopIndex = mLineIndexBySection[section + 1];
+        } else {
+            *lineStopIndex = mLines.size();
+        }
+    }
+}
+
+std::vector<std::string>::const_iterator SDP::section_begin(
+        size_t section) const {
+
+    size_t startLineIndex;
+    getSectionRange(section, &startLineIndex, nullptr /* lineStopIndex */);
+
+    return mLines.cbegin() + startLineIndex;
+}
+
+std::vector<std::string>::const_iterator SDP::section_end(
+        size_t section) const {
+
+    size_t stopLineIndex;
+    getSectionRange(section, nullptr /* lineStartIndex */, &stopLineIndex);
+
+    return mLines.cbegin() + stopLineIndex;
+}
+
+SDP::SectionEditor SDP::createSection() {
+    CHECK(!mNewSectionEditorActive);
+    mNewSectionEditorActive = true;
+
+    if (mInitCheck) {
+        clear();
+        mInitCheck = 0;
+    }
+
+    return SectionEditor(this, countSections());
+}
+
+SDP::SectionEditor SDP::appendToSection(size_t section) {
+    CHECK_LT(section, countSections());
+    return SectionEditor(this, section);
+}
+
+void SDP::commitSectionEdit(
+        size_t section, const std::vector<std::string> &lines) {
+
+    CHECK_LE(section, countSections());
+
+    if (section == countSections()) {
+        // This was an edit creating a new section.
+        mLineIndexBySection.push_back(mLines.size());
+
+        mLines.insert(mLines.end(), lines.begin(), lines.end());
+
+        mNewSectionEditorActive = false;
+        return;
+    }
+
+    mLines.insert(section_end(section), lines.begin(), lines.end());
+
+    if (section + 1 < countSections()) {
+        mLineIndexBySection[section + 1] += lines.size();
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+SDP::SectionEditor::SectionEditor(SDP *sdp, size_t section)
+    : mSDP(sdp),
+      mSection(section) {
+}
+
+SDP::SectionEditor::~SectionEditor() {
+    commit();
+}
+
+SDP::SectionEditor &SDP::SectionEditor::operator<<(std::string_view s) {
+    mBuffer.append(s);
+
+    return *this;
+}
+
+void SDP::SectionEditor::commit() {
+    if (mSDP) {
+        auto lines = SplitString(mBuffer, "\r\n");
+
+        mSDP->commitSectionEdit(mSection, lines);
+        mSDP = nullptr;
+    }
+}
diff --git a/host/frontend/gcastv2/webrtc/STUNMessage.cpp b/host/frontend/gcastv2/webrtc/STUNMessage.cpp
new file mode 100644
index 0000000..22bfb2a
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/STUNMessage.cpp
@@ -0,0 +1,457 @@
+#include <webrtc/STUNMessage.h>
+
+#include "Utils.h"
+
+#include <https/Support.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/Utils.h>
+
+#include <arpa/inet.h>
+
+#include <cstring>
+#include <iostream>
+#include <unordered_map>
+
+#if defined(TARGET_ANDROID) || defined(TARGET_ANDROID_DEVICE)
+#include <openssl/hmac.h>
+#else
+#include <Security/Security.h>
+#endif
+
+static constexpr uint8_t kMagicCookie[4] = { 0x21, 0x12, 0xa4, 0x42 };
+
+STUNMessage::STUNMessage(uint16_t type, const uint8_t transactionID[12])
+    : mIsValid(true),
+      mData(20),
+      mAddedMessageIntegrity(false) {
+    CHECK((type >> 14) == 0);
+
+    mData[0] = (type >> 8) & 0x3f;
+    mData[1] = type & 0xff;
+    mData[2] = 0;
+    mData[3] = 0;
+
+    memcpy(&mData[4], kMagicCookie, sizeof(kMagicCookie));
+
+    memcpy(&mData[8], transactionID, 12);
+}
+
+STUNMessage::STUNMessage(const void *data, size_t size)
+    : mIsValid(false),
+      mData(size) {
+    memcpy(mData.data(), data, size);
+
+    validate();
+}
+
+bool STUNMessage::isValid() const {
+    return mIsValid;
+}
+
+static uint16_t UINT16_AT(const void *_data) {
+    const uint8_t *data = static_cast<const uint8_t *>(_data);
+    return static_cast<uint16_t>(data[0]) << 8 | data[1];
+}
+
+uint16_t STUNMessage::type() const {
+    return UINT16_AT(mData.data());
+}
+
+void STUNMessage::addAttribute(uint16_t type, const void *data, size_t size) {
+    CHECK(!mAddedMessageIntegrity || type == 0x8028);
+
+    size_t alignedSize = (size + 3) & ~3;
+    CHECK_LE(alignedSize, 0xffffu);
+
+    size_t offset = mData.size();
+    mData.resize(mData.size() + 4 + alignedSize);
+
+    uint8_t *ptr = mData.data() + offset;
+    ptr[0] = type >> 8;
+    ptr[1] = type & 0xff;
+    ptr[2] = (size >> 8) & 0xff;
+    ptr[3] = size & 0xff;
+
+    if (size > 0) {
+        memcpy(&ptr[4], data, size);
+    }
+}
+
+void STUNMessage::addMessageIntegrityAttribute(std::string_view password) {
+    size_t offset = mData.size();
+
+    uint16_t truncatedLength = offset + 4;
+    mData[2] = (truncatedLength >> 8);
+    mData[3] = (truncatedLength & 0xff);
+
+#if defined(TARGET_ANDROID) || defined(TARGET_ANDROID_DEVICE)
+    uint8_t digest[20];
+    unsigned int digestLen = sizeof(digest);
+
+    HMAC(EVP_sha1(),
+         password.data(),
+         password.size(),
+         mData.data(),
+         offset,
+         digest,
+         &digestLen);
+
+    CHECK_EQ(digestLen, 20);
+    addAttribute(0x0008 /* MESSAGE-INTEGRITY */, digest, digestLen);
+#else
+    CFErrorRef err;
+    auto digest = SecDigestTransformCreate(
+            kSecDigestHMACSHA1, 20 /* digestLength */, &err);
+
+    CHECK(digest);
+
+    auto input = CFDataCreateWithBytesNoCopy(
+            kCFAllocatorDefault, mData.data(), offset, kCFAllocatorNull);
+
+    auto success = SecTransformSetAttribute(
+            digest, kSecTransformInputAttributeName, input, &err);
+
+    CFRelease(input);
+    input = nullptr;
+
+    CHECK(success);
+
+    auto key = CFDataCreateWithBytesNoCopy(
+            kCFAllocatorDefault,
+            reinterpret_cast<const UInt8 *>(password.data()),
+            password.size(),
+            kCFAllocatorNull);
+
+    success = SecTransformSetAttribute(
+            digest, kSecDigestHMACKeyAttribute, key, &err);
+
+    CFRelease(key);
+    key = nullptr;
+
+    CHECK(success);
+
+    auto output = SecTransformExecute(digest, &err);
+    CHECK(output);
+
+    auto outputAsData = static_cast<CFDataRef>(output);
+    CHECK_EQ(CFDataGetLength(outputAsData), 20);
+
+    addAttribute(
+            0x0008 /* MESSAGE-INTEGRITY */, CFDataGetBytePtr(outputAsData), 20);
+
+    CFRelease(output);
+    output = nullptr;
+
+    CFRelease(digest);
+    digest = nullptr;
+#endif
+
+    mAddedMessageIntegrity = true;
+}
+
+const uint8_t *STUNMessage::data() {
+    size_t size = mData.size() - 20;
+    CHECK_LE(size, 0xffffu);
+
+    mData[2] = (size >> 8) & 0xff;
+    mData[3] = size & 0xff;
+
+    return mData.data();
+}
+
+size_t STUNMessage::size() const {
+    return mData.size();
+}
+
+void STUNMessage::validate() {
+    if (mData.size() < 20) {
+        return;
+    }
+
+    const uint8_t *data = mData.data();
+
+    auto messageLength = UINT16_AT(data + 2);
+    if (messageLength != mData.size() - 20) {
+        return;
+    }
+
+    if (memcmp(kMagicCookie, &data[4], sizeof(kMagicCookie))) {
+        return;
+    }
+
+    bool sawMessageIntegrity = false;
+
+    data += 20;
+    size_t offset = 0;
+    while (offset + 4 <= messageLength) {
+        auto attrType = UINT16_AT(&data[offset]);
+
+        if (sawMessageIntegrity && attrType != 0x8028 /* FINGERPRINT */) {
+            return;
+        }
+
+        sawMessageIntegrity = (attrType == 0x0008 /* MESSAGE-INTEGRITY */);
+
+        auto attrLength = UINT16_AT(&data[offset + 2]);
+
+        if (offset + 4 + attrLength > messageLength) {
+            return;
+        }
+
+        offset += 4 + attrLength;
+        if (offset & 3) {
+            offset += 4 - (offset & 3);
+        }
+    }
+
+    if (offset != messageLength) {
+        return;
+    }
+
+    mAddedMessageIntegrity = sawMessageIntegrity;
+    mIsValid = true;
+}
+
+void STUNMessage::dump(std::optional<std::string_view> password) const {
+    CHECK(mIsValid);
+
+    const uint8_t *data = mData.data();
+
+    auto messageType = UINT16_AT(data);
+    auto messageLength = mData.size() - 20;
+
+    if (messageType == 0x0001) {
+        std::cout << "Binding Request";
+    } else if (messageType == 0x0101) {
+        std::cout << "Binding Response";
+    } else {
+        std::cout
+            << "Unknown message type "
+            << android::StringPrintf("0x%04x", messageType);
+    }
+
+    std::cout << std::endl;
+
+    data += 20;
+    size_t offset = 0;
+    while (offset + 4 <= messageLength) {
+        auto attrType = UINT16_AT(&data[offset]);
+        auto attrLength = UINT16_AT(&data[offset + 2]);
+
+        static const std::unordered_map<uint16_t, std::string> kAttrName {
+            { 0x0001, "MAPPED-ADDRESS" },
+            { 0x0006, "USERNAME" },
+            { 0x0008, "MESSAGE-INTEGRITY" },
+            { 0x0009, "ERROR-CODE" },
+            { 0x000A, "UNKNOWN-ATTRIBUTES" },
+            { 0x0014, "REALM" },
+            { 0x0015, "NONCE" },
+            { 0x0020, "XOR-MAPPED-ADDRESS" },
+            { 0x0024, "PRIORITY" },  // RFC8445
+            { 0x0025, "USE-CANDIDATE" },  // RFC8445
+            { 0x8022, "SOFTWARE" },
+            { 0x8023, "ALTERNATE-SERVER" },
+            { 0x8028, "FINGERPRINT" },
+            { 0x8029, "ICE-CONTROLLED" },  // RFC8445
+            { 0x802a, "ICE-CONTROLLING" },  // RFC8445
+        };
+
+        auto it = kAttrName.find(attrType);
+        if (it == kAttrName.end()) {
+            if (attrType <= 0x7fff) {
+                std::cout
+                    << "Unknown mandatory attribute type "
+                    << android::StringPrintf("0x%04x", attrType)
+                    << ":"
+                    << std::endl;
+            } else {
+                std::cout
+                    << "Unknown optional attribute type "
+                    << android::StringPrintf("0x%04x", attrType)
+                    << ":"
+                    << std::endl;
+            }
+        } else {
+            std::cout << "attribute '" << it->second << "':" << std::endl;
+        }
+
+        hexdump(&data[offset + 4], attrLength);
+
+        if (attrType == 8 /* MESSAGE_INTEGRITY */) {
+            if (attrLength != 20) {
+                LOG(WARNING)
+                    << "Message integrity attribute length mismatch."
+                    << " Expected 20, found "
+                    << attrLength;
+            } else if (password) {
+                auto success = verifyMessageIntegrity(offset + 20, *password);
+
+                if (!success) {
+                    LOG(WARNING) << "Message integrity check FAILED!";
+                }
+            }
+        } else if (attrType == 0x8028 /* FINGERPRINT */) {
+            if (attrLength != 4) {
+                LOG(WARNING)
+                    << "Fingerprint attribute length mismatch."
+                    << " Expected 4, found "
+                    << attrLength;
+            } else {
+                auto success = verifyFingerprint(offset + 20);
+
+                if (!success) {
+                    LOG(WARNING) << "Fingerprint check FAILED!";
+                }
+            }
+        }
+
+        offset += 4 + attrLength;
+        if (offset & 3) {
+            offset += 4 - (offset & 3);
+        }
+    }
+}
+
+bool STUNMessage::verifyMessageIntegrity(
+        size_t offset, std::string_view password) const {
+    // Password used as "short-term" credentials (RFC 5389).
+    // Technically the password would have to be SASLprep'ed...
+
+    std::vector<uint8_t> copy(offset);
+    memcpy(copy.data(), mData.data(), offset);
+
+    uint16_t truncatedLength = offset + 4;
+    copy[2] = (truncatedLength >> 8);
+    copy[3] = (truncatedLength & 0xff);
+
+#if defined(TARGET_ANDROID) || defined(TARGET_ANDROID_DEVICE)
+    uint8_t digest[20];
+    unsigned int digestLen = sizeof(digest);
+
+    HMAC(EVP_sha1(),
+         password.data(),
+         password.size(),
+         copy.data(),
+         copy.size(),
+         digest,
+         &digestLen);
+
+    CHECK_EQ(digestLen, 20);
+
+    bool success = !memcmp(
+            digest,
+            &mData[offset + 4],
+            digestLen);
+
+    return success;
+#else
+    CFErrorRef err;
+    auto digest = SecDigestTransformCreate(
+            kSecDigestHMACSHA1, 20 /* digestLength */, &err);
+
+    CHECK(digest);
+
+    auto input = CFDataCreateWithBytesNoCopy(
+            kCFAllocatorDefault, copy.data(), copy.size(), kCFAllocatorNull);
+
+    auto success = SecTransformSetAttribute(
+            digest, kSecTransformInputAttributeName, input, &err);
+
+    CFRelease(input);
+    input = nullptr;
+
+    CHECK(success);
+
+    auto key = CFDataCreateWithBytesNoCopy(
+            kCFAllocatorDefault,
+            reinterpret_cast<const UInt8 *>(password.data()),
+            password.size(),
+            kCFAllocatorNull);
+
+    success = SecTransformSetAttribute(
+            digest, kSecDigestHMACKeyAttribute, key, &err);
+
+    CFRelease(key);
+    key = nullptr;
+
+    CHECK(success);
+
+    auto output = SecTransformExecute(digest, &err);
+    CHECK(output);
+
+    success = !memcmp(
+            CFDataGetBytePtr(static_cast<CFDataRef>(output)),
+            &mData[offset + 4],
+            20);
+
+    CFRelease(output);
+    output = nullptr;
+
+    CFRelease(digest);
+    digest = nullptr;
+
+    return success;
+#endif
+}
+
+void STUNMessage::addFingerprint() {
+    size_t offset = mData.size();
+
+    // Pretend that we've added the FINGERPRINT attribute already.
+    uint16_t truncatedLength = offset + 4 + 4 - 20;
+    mData[2] = (truncatedLength >> 8);
+    mData[3] = (truncatedLength & 0xff);
+
+    uint32_t crc32 = htonl(computeCrc32(mData.data(), offset) ^ 0x5354554e);
+
+    addAttribute(0x8028 /* FINGERPRINT */, &crc32, sizeof(crc32));
+}
+
+bool STUNMessage::verifyFingerprint(size_t offset) const {
+    std::vector<uint8_t> copy(offset);
+    memcpy(copy.data(), mData.data(), offset);
+
+    copy[2] = ((mData.size() - 20) >> 8) & 0xff;
+    copy[3] = (mData.size() - 20) & 0xff;
+
+    uint32_t crc32 = htonl(computeCrc32(copy.data(), offset) ^ 0x5354554e);
+
+    // hexdump(&crc32, 4);
+
+    return !memcmp(&crc32, &mData[offset + 4], 4);
+}
+
+bool STUNMessage::findAttribute(
+        uint16_t type, const void **attrData, size_t *attrSize) const {
+    CHECK(mIsValid);
+
+    const uint8_t *data = mData.data();
+
+    auto messageLength = mData.size() - 20;
+
+    data += 20;
+    size_t offset = 0;
+    while (offset + 4 <= messageLength) {
+        auto attrType = UINT16_AT(&data[offset]);
+        auto attrLength = UINT16_AT(&data[offset + 2]);
+
+        if (attrType == type) {
+            *attrData = &data[offset + 4];
+            *attrSize = attrLength;
+
+            return true;
+        }
+
+        offset += 4 + attrLength;
+
+        if (offset & 3) {
+            offset += 4 - (offset & 3);
+        }
+    }
+
+    *attrData = nullptr;
+    *attrSize = 0;
+
+    return false;
+}
+
diff --git a/host/frontend/gcastv2/webrtc/ServerState.cpp b/host/frontend/gcastv2/webrtc/ServerState.cpp
new file mode 100644
index 0000000..821ad4c
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/ServerState.cpp
@@ -0,0 +1,312 @@
+#include <webrtc/ServerState.h>
+
+#include <webrtc/H264Packetizer.h>
+#include <webrtc/OpusPacketizer.h>
+#include <webrtc/VP8Packetizer.h>
+
+#ifdef TARGET_ANDROID
+#include <source/AudioSource.h>
+#include <source/CVMTouchSink.h>
+#include <source/FrameBufferSource.h>
+#include <source/VSOCTouchSink.h>
+
+#include "host/libs/config/cuttlefish_config.h"
+
+#include <gflags/gflags.h>
+
+DECLARE_int32(touch_fd);
+#endif
+
+#ifdef TARGET_ANDROID_DEVICE
+#include <source/DeviceFrameBufferSource.h>
+#include <source/DeviceTouchSink.h>
+#endif
+
+#define ENABLE_H264     0
+
+ServerState::ServerState(
+#ifdef TARGET_ANDROID_DEVICE
+        const sp<MyContext> &context,
+#endif
+        std::shared_ptr<RunLoop> runLoop, VideoFormat videoFormat)
+    :
+#ifdef TARGET_ANDROID_DEVICE
+        mContext(context),
+#endif
+        mRunLoop(runLoop),
+      mVideoFormat(videoFormat) {
+
+    // This is the list of ports we currently instruct the firewall to open.
+    mAvailablePorts.insert(
+            { 15550, 15551, 15552, 15553, 15554, 15555, 15556, 15557 });
+
+#ifdef TARGET_ANDROID
+    auto config = vsoc::CuttlefishConfig::Get();
+
+    mHostToGuestComms = std::make_shared<HostToGuestComms>(
+            mRunLoop,
+            false /* isServer */,
+            vsoc::GetDefaultPerInstanceVsockCid(),
+            HostToGuestComms::kPortMain,
+            [](const void *data, size_t size) {
+                (void)data;
+                LOG(VERBOSE) << "Received " << size << " bytes from guest.";
+            });
+
+    mHostToGuestComms->start();
+
+    changeResolution(1440 /* width */, 2880 /* height */, 524 /* dpi */);
+
+    android::FrameBufferSource::Format fbSourceFormat;
+    switch (videoFormat) {
+        case VideoFormat::H264:
+            fbSourceFormat = android::FrameBufferSource::Format::H264;
+            break;
+        case VideoFormat::VP8:
+            fbSourceFormat = android::FrameBufferSource::Format::VP8;
+            break;
+        default:
+            TRESPASS();
+    }
+
+    mFrameBufferSource =
+        std::make_shared<android::FrameBufferSource>(fbSourceFormat);
+
+    if (!config->enable_ivserver()) {
+        mFrameBufferComms = std::make_shared<HostToGuestComms>(
+                mRunLoop,
+                true /* isServer */,
+                VMADDR_CID_HOST,
+                HostToGuestComms::kPortVideo,
+                [this](const void *data, size_t size) {
+                    LOG(VERBOSE)
+                        << "Received packet of "
+                        << size
+                        << " bytes of data from hwcomposer HAL.";
+
+                    static_cast<android::FrameBufferSource *>(
+                            mFrameBufferSource.get())->injectFrame(data, size);
+                });
+
+        mFrameBufferComms->start();
+
+        int32_t screenParams[4];
+        screenParams[0] = config->x_res();
+        screenParams[1] = config->y_res();
+        screenParams[2] = config->dpi();
+        screenParams[3] = config->refresh_rate_hz();
+
+        mFrameBufferComms->send(
+                screenParams, sizeof(screenParams), false /* addFraming */);
+
+        static_cast<android::FrameBufferSource *>(
+                mFrameBufferSource.get())->setScreenParams(screenParams);
+    }
+
+    mAudioSource = std::make_shared<android::AudioSource>(
+            android::AudioSource::Format::OPUS);
+
+    if (!config->enable_ivserver()) {
+        mAudioComms = std::make_shared<HostToGuestComms>(
+                mRunLoop,
+                true /* isServer */,
+                VMADDR_CID_HOST,
+                HostToGuestComms::kPortAudio,
+                [this](const void *data, size_t size) {
+                    LOG(VERBOSE)
+                        << "Received packet of "
+                        << size
+                        << " bytes of data from audio HAL.";
+
+                    static_cast<android::AudioSource *>(
+                            mAudioSource.get())->inject(data, size);
+                });
+
+        mAudioComms->start();
+    }
+
+    if (config->enable_ivserver()) {
+        mTouchSink = std::make_shared<android::VSOCTouchSink>();
+    } else {
+        CHECK_GE(FLAGS_touch_fd, 0);
+
+        auto touchSink =
+            std::make_shared<android::CVMTouchSink>(mRunLoop, FLAGS_touch_fd);
+
+        touchSink->start();
+
+        mTouchSink = touchSink;
+    }
+#else
+    mLooper = new ALooper;
+    mLooper->start();
+
+    mFrameBufferSource = std::make_shared<android::DeviceFrameBufferSource>(
+            mContext,
+            mLooper,
+            android::DeviceFrameBufferSource::Type::VP8);
+
+    mAudioLooper = new ALooper;
+    mAudioLooper->start();
+
+    mAudioSource = std::make_shared<android::DeviceFrameBufferSource>(
+            mContext,
+            mAudioLooper,
+            android::DeviceFrameBufferSource::Type::OPUS);
+
+    mTouchSink = std::make_shared<android::DeviceTouchSink>();
+#endif
+}
+
+std::shared_ptr<Packetizer> ServerState::getVideoPacketizer() {
+    auto packetizer = mVideoPacketizer.lock();
+    if (!packetizer) {
+        switch (mVideoFormat) {
+#if ENABLE_H264
+            case VideoFormat::H264:
+            {
+                packetizer = std::make_shared<H264Packetizer>(
+                        mRunLoop, mFrameBufferSource);
+                break;
+            }
+#endif
+
+            case VideoFormat::VP8:
+            {
+                packetizer = std::make_shared<VP8Packetizer>(
+                        mRunLoop, mFrameBufferSource);
+                break;
+            }
+
+            default:
+                TRESPASS();
+        }
+
+        packetizer->run();
+
+        mVideoPacketizer = packetizer;
+    }
+
+    return packetizer;
+}
+
+std::shared_ptr<Packetizer> ServerState::getAudioPacketizer() {
+    auto packetizer = mAudioPacketizer.lock();
+    if (!packetizer) {
+        packetizer = std::make_shared<OpusPacketizer>(mRunLoop, mAudioSource);
+        packetizer->run();
+
+        mAudioPacketizer = packetizer;
+    }
+
+    return packetizer;
+}
+
+size_t ServerState::acquireHandlerId() {
+    size_t id = 0;
+    while (!mAllocatedHandlerIds.insert(id).second) {
+        ++id;
+    }
+
+    return id;
+}
+
+void ServerState::releaseHandlerId(size_t id) {
+    CHECK_EQ(mAllocatedHandlerIds.erase(id), 1);
+}
+
+uint16_t ServerState::acquirePort() {
+    std::lock_guard autoLock(mPortLock);
+
+    if (mAvailablePorts.empty()) {
+        return 0;
+    }
+
+    uint16_t port = *mAvailablePorts.begin();
+    mAvailablePorts.erase(mAvailablePorts.begin());
+
+    return port;
+}
+
+void ServerState::releasePort(uint16_t port) {
+    CHECK(port);
+
+    std::lock_guard autoLock(mPortLock);
+    mAvailablePorts.insert(port);
+}
+
+std::shared_ptr<android::StreamingSink> ServerState::getTouchSink() {
+    return mTouchSink;
+}
+
+#ifdef TARGET_ANDROID
+void ServerState::changeResolution(
+        int32_t width, int32_t height, int32_t densityDpi) {
+    LOG(INFO)
+        << "Requested dimensions: "
+        << width
+        << "x"
+        << height
+        << " @"
+        << densityDpi
+        << " dpi";
+
+    /* Arguments "width", "height" and "densityDpi" need to be matched to the
+     * native screen dimensions specified as "launch_cvd" arguments "x_res"
+     * and "y_res".
+     */
+
+    auto config = vsoc::CuttlefishConfig::Get();
+    const int nativeWidth = config->x_res();
+    const int nativeHeight = config->y_res();
+
+    const float ratio = (float)width / (float)height;
+    int32_t outWidth = nativeWidth;
+    int32_t outHeight = (int32_t)((float)outWidth / ratio);
+
+    if (outHeight > nativeHeight) {
+        outHeight = nativeHeight;
+        outWidth = (int32_t)((float)outHeight * ratio);
+    }
+
+    const int32_t outDensity =
+        (int32_t)((float)densityDpi * (float)outWidth / (float)width);
+
+    LOG(INFO)
+        << "Scaled dimensions: "
+        << outWidth
+        << "x"
+        << outHeight
+        << " @"
+        << outDensity
+        << " dpi";
+
+    enum class PacketType : uint8_t {
+        CHANGE_RESOLUTION = 6,
+    };
+
+    static constexpr size_t totalSize =
+        sizeof(PacketType) + 3 * sizeof(int32_t);
+
+    std::unique_ptr<uint8_t[]> packet(new uint8_t[totalSize]);
+    size_t offset = 0;
+
+    auto append = [ptr = packet.get(), &offset](
+            const void *data, size_t len) {
+        CHECK_LE(offset + len, totalSize);
+        memcpy(ptr + offset, data, len);
+        offset += len;
+    };
+
+    static constexpr PacketType type = PacketType::CHANGE_RESOLUTION;
+    append(&type, sizeof(type));
+    append(&outWidth, sizeof(outWidth));
+    append(&outHeight, sizeof(outHeight));
+    append(&outDensity, sizeof(outDensity));
+
+    CHECK_EQ(offset, totalSize);
+
+    mHostToGuestComms->send(packet.get(), totalSize);
+}
+
+#endif  // defined(TARGET_ANDROID)
diff --git a/host/frontend/gcastv2/webrtc/Utils.cpp b/host/frontend/gcastv2/webrtc/Utils.cpp
new file mode 100644
index 0000000..917c313
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/Utils.cpp
@@ -0,0 +1,140 @@
+#include "Utils.h"
+
+std::vector<std::string> SplitString(const std::string &s, char c) {
+    return SplitString(s, std::string(1 /* count */, c));
+}
+
+std::vector<std::string> SplitString(
+        const std::string &s, const std::string &separator) {
+    std::vector<std::string> pieces;
+
+    size_t startPos = 0;
+    size_t matchPos;
+    while ((matchPos = s.find(separator, startPos)) != std::string::npos) {
+        pieces.push_back(std::string(s, startPos, matchPos - startPos));
+        startPos = matchPos + separator.size();
+    }
+
+    if (startPos < s.size()) {
+        pieces.push_back(std::string(s, startPos));
+    }
+
+    return pieces;
+}
+
+bool StartsWith(const std::string &s, const std::string &prefix) {
+    return s.find(prefix) == 0;
+}
+
+void SET_U16(void *_dst, uint16_t x) {
+    uint8_t *dst = static_cast<uint8_t *>(_dst);
+    dst[0] = x >> 8;
+    dst[1] = x & 0xff;
+}
+
+void SET_U32(void *_dst, uint32_t x) {
+    uint8_t *dst = static_cast<uint8_t *>(_dst);
+    dst[0] = x >> 24;
+    dst[1] = (x >> 16) & 0xff;
+    dst[2] = (x >> 8) & 0xff;
+    dst[3] = x & 0xff;
+}
+
+#if 0
+static uint32_t crc32ForByte(uint32_t x) {
+    for (size_t i = 0; i < 8; ++i) {
+        if (x & 1) {
+            x = 0xedb88320 ^ (x >> 1);
+        } else {
+            x >>= 1;
+        }
+    }
+
+    return x;
+}
+
+uint32_t computeCrc32(const void *_data, size_t size) {
+    static uint32_t kTable[256];
+    if (!kTable[0]) {
+        for (size_t i = 0; i < 256; ++i) {
+            kTable[i] = crc32ForByte(i);
+        }
+    }
+
+    const uint8_t *data = static_cast<const uint8_t *>(_data);
+
+    uint32_t crc32 = 0xffffffff;
+    for (size_t i = 0; i < size; ++i) {
+        uint8_t x = data[i];
+
+        crc32 = (crc32 >> 8) ^ kTable[(crc32 & 0xff) ^ x];
+    }
+
+    return ~crc32;
+}
+#else
+static const uint32_t crc32_tab[] = {
+    0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
+    0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
+    0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
+    0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
+    0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
+    0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
+    0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
+    0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
+    0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
+    0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
+    0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
+    0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
+    0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
+    0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
+    0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
+    0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
+    0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
+    0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
+    0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
+    0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
+    0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
+    0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
+    0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
+    0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
+    0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
+    0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
+    0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
+    0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
+    0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
+    0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
+    0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
+    0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
+    0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
+    0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
+    0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
+    0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
+    0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
+    0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
+    0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
+    0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
+    0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
+    0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
+    0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
+};
+
+uint32_t computeCrc32(const void *_data, size_t size) {
+    const uint8_t *data = static_cast<const uint8_t *>(_data);
+
+    uint32_t crc = 0xffffffff;
+
+    for (size_t i = 0; i < size; ++i) {
+        uint32_t lkp = crc32_tab[(crc ^ data[i]) & 0xFF];
+#if 0
+        bool wlm2009_stupid_crc32_typo = false;
+        if (lkp == 0x8bbeb8ea && wlm2009_stupid_crc32_typo) {
+            lkp = 0x8bbe8ea;
+        }
+#endif
+        crc =  lkp ^ (crc >> 8);
+    }
+
+    return crc ^ 0xffffffff;
+}
+#endif
diff --git a/host/frontend/gcastv2/webrtc/Utils.h b/host/frontend/gcastv2/webrtc/Utils.h
new file mode 100644
index 0000000..fd812c3
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/Utils.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+std::vector<std::string> SplitString(const std::string &s, char c);
+
+std::vector<std::string> SplitString(
+        const std::string &s, const std::string &separator);
+
+bool StartsWith(const std::string &s, const std::string &prefix);
+
+void SET_U16(void *_dst, uint16_t x);
+void SET_U32(void *_dst, uint32_t x);
+
+uint32_t computeCrc32(const void *_data, size_t size);
diff --git a/host/frontend/gcastv2/webrtc/VP8Packetizer.cpp b/host/frontend/gcastv2/webrtc/VP8Packetizer.cpp
new file mode 100644
index 0000000..d5cae1c
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/VP8Packetizer.cpp
@@ -0,0 +1,145 @@
+#include <webrtc/VP8Packetizer.h>
+
+#include "Utils.h"
+
+#include <webrtc/RTPSocketHandler.h>
+
+#include <https/SafeCallbackable.h>
+#include <https/Support.h>
+
+using namespace android;
+
+VP8Packetizer::VP8Packetizer(
+        std::shared_ptr<RunLoop> runLoop,
+        std::shared_ptr<StreamingSource> frameBufferSource)
+    : mRunLoop(runLoop),
+      mFrameBufferSource(frameBufferSource),
+      mNumSamplesRead(0),
+      mStartTimeMedia(0) {
+}
+
+VP8Packetizer::~VP8Packetizer() {
+    if (mFrameBufferSource) {
+        mFrameBufferSource->stop();
+    }
+}
+
+void VP8Packetizer::run() {
+    auto weak_this = std::weak_ptr<VP8Packetizer>(shared_from_this());
+
+    mFrameBufferSource->setCallback(
+            [weak_this](const sp<ABuffer> &accessUnit) {
+                auto me = weak_this.lock();
+                if (me) {
+                    me->mRunLoop->post(
+                            makeSafeCallback(
+                                me.get(), &VP8Packetizer::onFrame, accessUnit));
+                }
+            });
+
+    mFrameBufferSource->start();
+}
+
+void VP8Packetizer::onFrame(const sp<ABuffer> &accessUnit) {
+    int64_t timeUs;
+    CHECK(accessUnit->meta()->findInt64("timeUs", &timeUs));
+
+    auto now = std::chrono::steady_clock::now();
+
+    if (mNumSamplesRead == 0) {
+        mStartTimeMedia = timeUs;
+        mStartTimeReal = now;
+    }
+
+    ++mNumSamplesRead;
+
+    LOG(VERBOSE)
+        << "got accessUnit of size "
+        << accessUnit->size()
+        << " at time "
+        << timeUs;
+
+    packetize(accessUnit, timeUs);
+}
+
+void VP8Packetizer::packetize(const sp<ABuffer> &accessUnit, int64_t timeUs) {
+    static constexpr uint8_t PT = 96;
+    static constexpr uint32_t SSRC = 0xdeadbeef;
+
+    // XXX Retransmission packets add 2 bytes (for the original seqNum), should
+    // probably reserve that amount in the original packets so we don't exceed
+    // the MTU on retransmission.
+    static const size_t kMaxSRTPPayloadSize =
+        RTPSocketHandler::kMaxUDPPayloadSize - SRTP_MAX_TRAILER_LEN;
+
+    const uint8_t *src = accessUnit->data();
+    size_t srcSize = accessUnit->size();
+
+    uint32_t rtpTime = ((timeUs - mStartTimeMedia) * 9) / 100;
+
+    LOG(VERBOSE) << "got accessUnit of size " << srcSize;
+
+    size_t srcOffset = 0;
+    while (srcOffset < srcSize) {
+        size_t packetSize = 12;  // generic RTP header
+
+        packetSize += 1;  // VP8 Payload Descriptor
+
+        auto copy = std::min(srcSize - srcOffset, kMaxSRTPPayloadSize - packetSize);
+
+        packetSize += copy;
+
+        std::vector<uint8_t> packet(packetSize);
+        uint8_t *dst = packet.data();
+
+        dst[0] = 0x80;
+
+        dst[1] = PT;
+        if (srcOffset + copy == srcSize) {
+            dst[1] |= 0x80;  // (M)ark
+        }
+
+        SET_U16(&dst[2], 0);  // seqNum
+        SET_U32(&dst[4], rtpTime);
+        SET_U32(&dst[8], SSRC);
+
+        size_t dstOffset = 12;
+
+        // VP8 Payload Descriptor
+        dst[dstOffset++] = (srcOffset == 0) ? 0x10 : 0x00;  // S
+
+        memcpy(&dst[dstOffset], &src[srcOffset], copy);
+        dstOffset += copy;
+
+        CHECK_EQ(dstOffset, packetSize);
+
+#if 0
+        LOG(INFO) << "Sending packet of size " << packetSize;
+        hexdump(dst, std::min(128ul, packetSize));
+#endif
+
+        srcOffset += copy;
+
+        queueRTPDatagram(&packet);
+    }
+}
+
+uint32_t VP8Packetizer::rtpNow() const {
+    if (mNumSamplesRead == 0) {
+        return 0;
+    }
+
+    auto now = std::chrono::steady_clock::now();
+    auto timeSinceStart = now - mStartTimeReal;
+
+    auto us_since_start =
+        std::chrono::duration_cast<std::chrono::microseconds>(
+                timeSinceStart).count();
+
+    return (us_since_start * 9) / 100;
+}
+
+android::status_t VP8Packetizer::requestIDRFrame() {
+    return mFrameBufferSource->requestIDRFrame();
+}
+
diff --git a/host/frontend/gcastv2/webrtc/certs/create_certs.sh b/host/frontend/gcastv2/webrtc/certs/create_certs.sh
new file mode 100755
index 0000000..9f85e40
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/certs/create_certs.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+# As explained in
+#  https://gist.github.com/darrenjs/4645f115d10aa4b5cebf57483ec82eca
+
+openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
+openssl rsa -passin pass:x -in server.pass.key -out server.key
+rm -f server.pass.key
+
+openssl req \
+    -subj "/C=US/ST=California/L=Santa Clara/O=Beyond Aggravated/CN=localhost" \
+    -new -key server.key -out server.csr
+
+openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt
+rm -f server.csr
+
+# Now create the list of certificates we trust as a client.
+
+rm trusted.pem
+
+# For now we just trust our own server.
+openssl x509 -in server.crt -text >> trusted.pem
+
+# Also add the system standard CA cert chain.
+# cat /opt/local/etc/openssl/cert.pem >> trusted.pem
+
+# Convert .pem to .der
+# openssl x509 -outform der -in trusted.pem -out trusted.der
+
+# Convert .crt and .key to .p12 for use by Security.framework
+# Enter password "foo"!
+openssl pkcs12 -export -inkey server.key -in server.crt -name localhost -out server.p12
diff --git a/host/frontend/gcastv2/webrtc/certs/server.crt b/host/frontend/gcastv2/webrtc/certs/server.crt
new file mode 100644
index 0000000..81759be
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/certs/server.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDTDCCAjQCCQCsLGBNpNbWCjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJV
+UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAY
+BgNVBAoMEUJleW9uZCBBZ2dyYXZhdGVkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcN
+MTkwNTA4MjAxMTQ5WhcNMjAwNTA3MjAxMTQ5WjBoMQswCQYDVQQGEwJVUzETMBEG
+A1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNVBAoM
+EUJleW9uZCBBZ2dyYXZhdGVkMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQD3c9E6JtTUro8MWBYRtjJ6k01ORfROTsJz
+vbL5V3IV6MyUqGKbhRuN2SP/XsVZoUCmeFsCVF++G3mLmYetdUtb/ZwIgF/sN5aW
+oh//YDR/T2kepMjk73kvQ7R6h5bB5vQIbpbP+piJR1S7HxlXEgwyLuXmKgVNF6tU
+WJt/vRFLRDiz0Bn3GklE5yOgJGeeU3CYioatAiKE6b/yPu5pcM7Kj4tn9hu5COO2
+89Wol3od+I3pSkd9tXypdfw1txdAi0o9P7uIDIDSIDwf7RyCoxojZ7yUxO2Q0ld+
+KFJqioSpFIscIxR1BaTIrJaSCU7Hn82ihODS2ue4OaoAJ2qH8JtVAgMBAAEwDQYJ
+KoZIhvcNAQELBQADggEBAPQ+7bXvy/RSx9lgSAiO/R5ep3Si2xWUe0hYivmQ1bX2
+80FpDP8tZXtAkIhpyWdiS0aYBMySLDDDiDl2xUWfxZO0NnOcVJ17jZ2sJxhqKksW
+NMTLb7dCr7kUS2+FOuXwR+Yeb77up2e54lXLuiKVWevFAUVc8Xhgq/sNz2rwt5iG
+XFcLCXoEgLwHnd7LBR8y6IsEfVW5UVSWpFQPODdcYtVgaWYo7TYghZjzEya8VIc7
+HgHlH/1Uj9yvh+eY2hLoLwukZRV/CnMbSk8LLgTYuEeLfPuSnCTERrydMEyRcF3H
+ljCthgV7YRt8cQovsVZBvuMRJYzl4hM3ema00Px7SD8=
+-----END CERTIFICATE-----
diff --git a/host/frontend/gcastv2/webrtc/certs/server.key b/host/frontend/gcastv2/webrtc/certs/server.key
new file mode 100644
index 0000000..616a4ce
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/certs/server.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpgIBAAKCAQEA93PROibU1K6PDFgWEbYyepNNTkX0Tk7Cc72y+VdyFejMlKhi
+m4Ubjdkj/17FWaFApnhbAlRfvht5i5mHrXVLW/2cCIBf7DeWlqIf/2A0f09pHqTI
+5O95L0O0eoeWweb0CG6Wz/qYiUdUux8ZVxIMMi7l5ioFTRerVFibf70RS0Q4s9AZ
+9xpJROcjoCRnnlNwmIqGrQIihOm/8j7uaXDOyo+LZ/YbuQjjtvPVqJd6HfiN6UpH
+fbV8qXX8NbcXQItKPT+7iAyA0iA8H+0cgqMaI2e8lMTtkNJXfihSaoqEqRSLHCMU
+dQWkyKyWkglOx5/NooTg0trnuDmqACdqh/CbVQIDAQABAoIBAQCnBGrxvwfjzTYL
+9OBgcAM+LHH/JMQynoIssJs+JEGCfDCpHcYAhiUE5syfLo4xYt9J/O4gcmZ04AJ3
+sNacwxBsNI6+Rjd4LkTbwu2p5ntIeobPAhX+P4wh1KbaFO4yTfnkPxBXrCKMdbLA
+4cqutCW7MWBGq5IMaK9hLLU30Jr9muiEIQ8tnexghJ4SiHnpju471KD9N77dRVZp
+TmUnIlJLqYMNoPj0nLwy6p1isq1KSg56j27OooL8piqOp2cy82GqjNeCfFcW74dJ
+lYmxILLseJiJvM2nlfFdSOvSOtaBkSG5oKsOO1K56u3cXhwSXChu5b8ewNjG5d+Y
+KY5obpV5AoGBAP/nJGxv7ljsg/pAqfWOwR5g6Iz9JxpgweubVt9fvnS+2Pj4akrJ
+1NZZCB/9zFbYf2Of2VMBdrNw7eS2+QDwj2TYPBjIH3WMPSByyyQaNna47b26Sxu2
+kCMhkoEMvwXEEQrLZ7I/sHgzYIEGg/9yLUJmzfdqpACU4bA1zkzKQ6tnAoGBAPeL
+2qn7ch3vlVLK46tm99Jkbw2SxoJajlZ4p2kpe/EexyqInfZJ58IKQNonqYNWMOwg
+aLeoyf+XsWgnuS4njMBPLmGTa/Pibs2b/0jrHP3mp2LVrRaL+wOUDM/gzmt7NeD+
+zZ8fD4Lsj8lXLfys5M5HrosF8c3TT8odFALjLwnjAoGBAKmH3qh8CsIchl6O4knM
+tgHDH6zvtS0TdsT4lzfKfSlomeNu5zP+vCL4vpo7EFlkehhs+JO1/4ZnRSLlWNcX
+h1e+rSmZwsWkD4bkpdGYEAbdAptTxJhqfNjZT+5wnEhcmRG2qU78RJONLdysjVv4
+ryUzaDYGDvpXp6CONMrIoMX3AoGBAJV/bbg4dauklD6i7yoFjmcOZo8A9EenHs0U
+Iq588jAlUUzbouIpsgBapt3ZFCOQOw1vaS55jjyAxRBM5SX9lqBRcYZWPNzWA+rC
+akMEUsb3tGEZAGZcdWSs1av5bVA14c0WtOGDJaAA87k5oDk3xRra6YtmNKkEE+zQ
+8NPple/XAoGBAJMpXdSP+Hgakv//HaQaBcenHk1f8v4b8t48NnXjou3ojfO8Wzgr
+odwlninHk1PaQD0XnFIISMVD9d2aSX8X1YQVHUQ9IQiA4hLy2lxZbP2K+LEaVKDm
+r7cjRKixJTIGm6vZK8pr1l5XD3657fQte4YLei5XA+i/UgFZyIA0KsKo
+-----END RSA PRIVATE KEY-----
diff --git a/host/frontend/gcastv2/webrtc/certs/server.p12 b/host/frontend/gcastv2/webrtc/certs/server.p12
new file mode 100644
index 0000000..3d0d595
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/certs/server.p12
Binary files differ
diff --git a/host/frontend/gcastv2/webrtc/certs/trusted.pem b/host/frontend/gcastv2/webrtc/certs/trusted.pem
new file mode 100644
index 0000000..b2080b4
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/certs/trusted.pem
@@ -0,0 +1,69 @@
+Certificate:
+    Data:
+        Version: 1 (0x0)
+        Serial Number: 12406396960093165066 (0xac2c604da4d6d60a)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: C=US, ST=California, L=Santa Clara, O=Beyond Aggravated, CN=localhost
+        Validity
+            Not Before: May  8 20:11:49 2019 GMT
+            Not After : May  7 20:11:49 2020 GMT
+        Subject: C=US, ST=California, L=Santa Clara, O=Beyond Aggravated, CN=localhost
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:f7:73:d1:3a:26:d4:d4:ae:8f:0c:58:16:11:b6:
+                    32:7a:93:4d:4e:45:f4:4e:4e:c2:73:bd:b2:f9:57:
+                    72:15:e8:cc:94:a8:62:9b:85:1b:8d:d9:23:ff:5e:
+                    c5:59:a1:40:a6:78:5b:02:54:5f:be:1b:79:8b:99:
+                    87:ad:75:4b:5b:fd:9c:08:80:5f:ec:37:96:96:a2:
+                    1f:ff:60:34:7f:4f:69:1e:a4:c8:e4:ef:79:2f:43:
+                    b4:7a:87:96:c1:e6:f4:08:6e:96:cf:fa:98:89:47:
+                    54:bb:1f:19:57:12:0c:32:2e:e5:e6:2a:05:4d:17:
+                    ab:54:58:9b:7f:bd:11:4b:44:38:b3:d0:19:f7:1a:
+                    49:44:e7:23:a0:24:67:9e:53:70:98:8a:86:ad:02:
+                    22:84:e9:bf:f2:3e:ee:69:70:ce:ca:8f:8b:67:f6:
+                    1b:b9:08:e3:b6:f3:d5:a8:97:7a:1d:f8:8d:e9:4a:
+                    47:7d:b5:7c:a9:75:fc:35:b7:17:40:8b:4a:3d:3f:
+                    bb:88:0c:80:d2:20:3c:1f:ed:1c:82:a3:1a:23:67:
+                    bc:94:c4:ed:90:d2:57:7e:28:52:6a:8a:84:a9:14:
+                    8b:1c:23:14:75:05:a4:c8:ac:96:92:09:4e:c7:9f:
+                    cd:a2:84:e0:d2:da:e7:b8:39:aa:00:27:6a:87:f0:
+                    9b:55
+                Exponent: 65537 (0x10001)
+    Signature Algorithm: sha256WithRSAEncryption
+         f4:3e:ed:b5:ef:cb:f4:52:c7:d9:60:48:08:8e:fd:1e:5e:a7:
+         74:a2:db:15:94:7b:48:58:8a:f9:90:d5:b5:f6:f3:41:69:0c:
+         ff:2d:65:7b:40:90:88:69:c9:67:62:4b:46:98:04:cc:92:2c:
+         30:c3:88:39:76:c5:45:9f:c5:93:b4:36:73:9c:54:9d:7b:8d:
+         9d:ac:27:18:6a:2a:4b:16:34:c4:cb:6f:b7:42:af:b9:14:4b:
+         6f:85:3a:e5:f0:47:e6:1e:6f:be:ee:a7:67:b9:e2:55:cb:ba:
+         22:95:59:eb:c5:01:45:5c:f1:78:60:ab:fb:0d:cf:6a:f0:b7:
+         98:86:5c:57:0b:09:7a:04:80:bc:07:9d:de:cb:05:1f:32:e8:
+         8b:04:7d:55:b9:51:54:96:a4:54:0f:38:37:5c:62:d5:60:69:
+         66:28:ed:36:20:85:98:f3:13:26:bc:54:87:3b:1e:01:e5:1f:
+         fd:54:8f:dc:af:87:e7:98:da:12:e8:2f:0b:a4:65:15:7f:0a:
+         73:1b:4a:4f:0b:2e:04:d8:b8:47:8b:7c:fb:92:9c:24:c4:46:
+         bc:9d:30:4c:91:70:5d:c7:96:30:ad:86:05:7b:61:1b:7c:71:
+         0a:2f:b1:56:41:be:e3:11:25:8c:e5:e2:13:37:7a:66:b4:d0:
+         fc:7b:48:3f
+-----BEGIN CERTIFICATE-----
+MIIDTDCCAjQCCQCsLGBNpNbWCjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJV
+UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAY
+BgNVBAoMEUJleW9uZCBBZ2dyYXZhdGVkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcN
+MTkwNTA4MjAxMTQ5WhcNMjAwNTA3MjAxMTQ5WjBoMQswCQYDVQQGEwJVUzETMBEG
+A1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNVBAoM
+EUJleW9uZCBBZ2dyYXZhdGVkMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQD3c9E6JtTUro8MWBYRtjJ6k01ORfROTsJz
+vbL5V3IV6MyUqGKbhRuN2SP/XsVZoUCmeFsCVF++G3mLmYetdUtb/ZwIgF/sN5aW
+oh//YDR/T2kepMjk73kvQ7R6h5bB5vQIbpbP+piJR1S7HxlXEgwyLuXmKgVNF6tU
+WJt/vRFLRDiz0Bn3GklE5yOgJGeeU3CYioatAiKE6b/yPu5pcM7Kj4tn9hu5COO2
+89Wol3od+I3pSkd9tXypdfw1txdAi0o9P7uIDIDSIDwf7RyCoxojZ7yUxO2Q0ld+
+KFJqioSpFIscIxR1BaTIrJaSCU7Hn82ihODS2ue4OaoAJ2qH8JtVAgMBAAEwDQYJ
+KoZIhvcNAQELBQADggEBAPQ+7bXvy/RSx9lgSAiO/R5ep3Si2xWUe0hYivmQ1bX2
+80FpDP8tZXtAkIhpyWdiS0aYBMySLDDDiDl2xUWfxZO0NnOcVJ17jZ2sJxhqKksW
+NMTLb7dCr7kUS2+FOuXwR+Yeb77up2e54lXLuiKVWevFAUVc8Xhgq/sNz2rwt5iG
+XFcLCXoEgLwHnd7LBR8y6IsEfVW5UVSWpFQPODdcYtVgaWYo7TYghZjzEya8VIc7
+HgHlH/1Uj9yvh+eY2hLoLwukZRV/CnMbSk8LLgTYuEeLfPuSnCTERrydMEyRcF3H
+ljCthgV7YRt8cQovsVZBvuMRJYzl4hM3ema00Px7SD8=
+-----END CERTIFICATE-----
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/AdbWebSocketHandler.h b/host/frontend/gcastv2/webrtc/include/webrtc/AdbWebSocketHandler.h
new file mode 100644
index 0000000..4983835
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/AdbWebSocketHandler.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <https/WebSocketHandler.h>
+#include <https/RunLoop.h>
+
+#include <memory>
+
+struct AdbWebSocketHandler
+    : public WebSocketHandler,
+      public std::enable_shared_from_this<AdbWebSocketHandler> {
+
+    explicit AdbWebSocketHandler(
+            std::shared_ptr<RunLoop> runLoop,
+            const std::string &adb_host_and_port);
+
+    ~AdbWebSocketHandler() override;
+
+    void run();
+
+    int handleMessage(
+            uint8_t headerByte, const uint8_t *msg, size_t len) override;
+
+private:
+    struct AdbConnection;
+
+    std::shared_ptr<RunLoop> mRunLoop;
+    std::shared_ptr<AdbConnection> mAdbConnection;
+
+    int mSocket;
+
+    int setupSocket(const std::string &adb_host_and_port);
+};
+
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/DTLS.h b/host/frontend/gcastv2/webrtc/include/webrtc/DTLS.h
new file mode 100644
index 0000000..ea8ffeb
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/DTLS.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include <https/RunLoop.h>
+
+#include <openssl/bio.h>
+#include <openssl/ssl.h>
+
+#include <functional>
+#include <memory>
+#include <netinet/in.h>
+#include <optional>
+#include <vector>
+
+#include <srtp2/srtp.h>
+
+struct RTPSocketHandler;
+
+struct DTLS : public std::enable_shared_from_this<DTLS> {
+    static void Init();
+
+    enum class Mode {
+        ACCEPT,
+        CONNECT
+    };
+
+    explicit DTLS(
+            std::shared_ptr<RTPSocketHandler> handler,
+            Mode mode,
+            std::shared_ptr<X509> certificate,
+            std::shared_ptr<EVP_PKEY> key,
+            const std::string &remoteFingerprint,
+            bool useSRTP);
+
+    ~DTLS();
+
+    void connect(const sockaddr_storage &remoteAddr);
+    void inject(const uint8_t *data, size_t size);
+
+    size_t protect(void *data, size_t size, bool isRTP);
+    size_t unprotect(void *data, size_t size, bool isRTP);
+
+    // Returns -EAGAIN if no data is currently available.
+    ssize_t readApplicationData(void *data, size_t size);
+
+    ssize_t writeApplicationData(const void *data, size_t size);
+
+private:
+    enum class State {
+        UNINITIALIZED,
+        CONNECTING,
+        CONNECTED,
+
+    } mState;
+
+    std::weak_ptr<RTPSocketHandler> mHandler;
+    Mode mMode;
+    std::string mRemoteFingerprint;
+    bool mUseSRTP;
+
+    SSL_CTX *mCtx;
+    SSL *mSSL;
+
+    // These are owned by the SSL object.
+    BIO *mBioR, *mBioW;
+
+    sockaddr_storage mRemoteAddr;
+
+    srtp_t mSRTPInbound, mSRTPOutbound;
+
+    static int OnVerifyPeerCertificate(int ok, X509_STORE_CTX *ctx);
+
+    void doTheThing(int res);
+    void queueOutputDataFromDTLS();
+    void tryConnecting();
+
+    void getKeyingMaterial();
+
+    static void CreateSRTPSession(
+            srtp_t *session,
+            const std::string &keyAndSalt,
+            srtp_ssrc_type_t direction);
+
+    bool useCertificate(std::shared_ptr<X509> certificate);
+    bool usePrivateKey(std::shared_ptr<EVP_PKEY> key);
+};
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/G711Packetizer.h b/host/frontend/gcastv2/webrtc/include/webrtc/G711Packetizer.h
new file mode 100644
index 0000000..19adda4
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/G711Packetizer.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "Packetizer.h"
+
+#include <https/RunLoop.h>
+
+#include <memory>
+
+#include <media/stagefright/foundation/ABuffer.h>
+#include <source/StreamingSource.h>
+
+struct G711Packetizer
+    : public Packetizer, public std::enable_shared_from_this<G711Packetizer> {
+
+    using StreamingSource = android::StreamingSource;
+
+    enum class Mode {
+        ALAW,
+        ULAW
+    };
+    explicit G711Packetizer(
+            Mode mode,
+            std::shared_ptr<RunLoop> runLoop,
+            std::shared_ptr<StreamingSource> audioSource);
+
+    void run() override;
+    uint32_t rtpNow() const override;
+    android::status_t requestIDRFrame() override;
+
+private:
+    template<class T> using sp = android::sp<T>;
+    using ABuffer = android::ABuffer;
+
+    Mode mMode;
+    std::shared_ptr<RunLoop> mRunLoop;
+
+    std::shared_ptr<StreamingSource> mAudioSource;
+
+    size_t mNumSamplesRead;
+
+    std::chrono::time_point<std::chrono::steady_clock> mStartTimeReal;
+    int64_t mStartTimeMedia;
+    bool mFirstInTalkspurt;
+
+    void onFrame(const sp<ABuffer> &accessUnit);
+
+    void packetize(const sp<ABuffer> &accessUnit, int64_t timeUs);
+};
+
+
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/H264Packetizer.h b/host/frontend/gcastv2/webrtc/include/webrtc/H264Packetizer.h
new file mode 100644
index 0000000..ea70990
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/H264Packetizer.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include "Packetizer.h"
+
+#include <https/RunLoop.h>
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/NuMediaExtractor.h>
+
+#include <memory>
+
+#include <source/StreamingSource.h>
+
+struct H264Packetizer
+    : public Packetizer, public std::enable_shared_from_this<H264Packetizer> {
+    using StreamingSource = android::StreamingSource;
+
+    explicit H264Packetizer(
+            std::shared_ptr<RunLoop> runLoop,
+            std::shared_ptr<StreamingSource> frameBufferSource);
+
+    void run() override;
+    uint32_t rtpNow() const override;
+    android::status_t requestIDRFrame() override;
+
+private:
+    using ABuffer = android::ABuffer;
+    template<class T> using sp = android::sp<T>;
+
+    std::shared_ptr<RunLoop> mRunLoop;
+
+    std::shared_ptr<StreamingSource> mFrameBufferSource;
+
+    size_t mNumSamplesRead;
+
+    std::chrono::time_point<std::chrono::steady_clock> mStartTimeReal;
+    int64_t mStartTimeMedia;
+
+    void onFrame(const sp<ABuffer> &accessUnit);
+
+    void packetize(const sp<ABuffer> &accessUnit, int64_t timeUs);
+};
+
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/MyWebSocketHandler.h b/host/frontend/gcastv2/webrtc/include/webrtc/MyWebSocketHandler.h
new file mode 100644
index 0000000..18f4296
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/MyWebSocketHandler.h
@@ -0,0 +1,95 @@
+#pragma once
+
+#include <webrtc/RTPSession.h>
+#include <webrtc/RTPSocketHandler.h>
+#include <webrtc/SDP.h>
+#include <webrtc/ServerState.h>
+
+#include <https/WebSocketHandler.h>
+#include <https/RunLoop.h>
+#include <media/stagefright/foundation/JSONObject.h>
+#include <source/StreamingSink.h>
+
+#include <memory>
+#include <optional>
+#include <sstream>
+#include <string>
+#include <vector>
+
+struct MyWebSocketHandler
+    : public WebSocketHandler,
+      public std::enable_shared_from_this<MyWebSocketHandler> {
+
+    explicit MyWebSocketHandler(
+            std::shared_ptr<RunLoop> runLoop,
+            std::shared_ptr<ServerState> serverState,
+            size_t handlerId);
+
+    ~MyWebSocketHandler() override;
+
+    int handleMessage(
+            uint8_t headerByte, const uint8_t *msg, size_t len) override;
+
+private:
+    template<class T> using sp = android::sp<T>;
+    using JSONObject = android::JSONObject;
+
+    enum OptionBits : uint32_t {
+        disableAudio                        = 1,
+        bundleTracks                        = 2,
+        enableData                          = 4,
+        useSingleCertificateForAllTracks    = 8,
+    };
+
+    using StreamingSink = android::StreamingSink;
+
+    std::shared_ptr<RunLoop> mRunLoop;
+    std::shared_ptr<ServerState> mServerState;
+    size_t mId;
+    uint32_t mOptions;
+
+    // Vector has the same ordering as the media entries in the SDP, i.e.
+    // vector index is "mlineIndex". (unless we are bundling, in which case
+    // there is only a single session).
+    std::vector<std::shared_ptr<RTPSession>> mSessions;
+
+    SDP mOfferedSDP;
+    std::vector<std::shared_ptr<RTPSocketHandler>> mRTPs;
+
+    std::shared_ptr<StreamingSink> mTouchSink;
+
+    std::pair<std::shared_ptr<X509>, std::shared_ptr<EVP_PKEY>>
+        mCertificateAndKey;
+
+    // Pass -1 for mlineIndex to access the "general" section.
+    std::optional<std::string> getSDPValue(
+            ssize_t mlineIndex,
+            std::string_view key,
+            bool fallthroughToGeneralSection) const;
+
+    std::string getRemotePassword(size_t mlineIndex) const;
+    std::string getRemoteUFrag(size_t mlineIndex) const;
+    std::string getRemoteFingerprint(size_t mlineIndex) const;
+
+    bool getCandidate(int32_t mid);
+
+    static std::pair<std::shared_ptr<X509>, std::shared_ptr<EVP_PKEY>>
+        CreateDTLSCertificateAndKey();
+
+    std::pair<std::string, std::string> createUniqueUFragAndPassword();
+
+    void parseOptions(const std::string &pathAndQuery);
+    size_t countTracks() const;
+
+    void prepareSessions();
+
+    void emitTrackIceOptionsAndFingerprint(
+            std::stringstream &ss, size_t mlineIndex) const;
+
+    // Returns -1 on error.
+    ssize_t mlineIndexForMid(int32_t mid) const;
+
+    static void CreateRandomIceCharSequence(char *dst, size_t size);
+};
+
+
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/OpusPacketizer.h b/host/frontend/gcastv2/webrtc/include/webrtc/OpusPacketizer.h
new file mode 100644
index 0000000..737ecba
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/OpusPacketizer.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "Packetizer.h"
+
+#include <https/RunLoop.h>
+
+#include <memory>
+
+#include <media/stagefright/foundation/ABuffer.h>
+#include <source/StreamingSource.h>
+
+struct OpusPacketizer
+    : public Packetizer, public std::enable_shared_from_this<OpusPacketizer> {
+
+    using StreamingSource = android::StreamingSource;
+
+    explicit OpusPacketizer(
+            std::shared_ptr<RunLoop> runLoop,
+            std::shared_ptr<StreamingSource> audioSource);
+
+    void run() override;
+    uint32_t rtpNow() const override;
+    android::status_t requestIDRFrame() override;
+
+private:
+    template<class T> using sp = android::sp<T>;
+    using ABuffer = android::ABuffer;
+
+    std::shared_ptr<RunLoop> mRunLoop;
+
+    std::shared_ptr<StreamingSource> mAudioSource;
+
+    size_t mNumSamplesRead;
+
+    std::chrono::time_point<std::chrono::steady_clock> mStartTimeReal;
+    int64_t mStartTimeMedia;
+    bool mFirstInTalkspurt;
+
+    void onFrame(const sp<ABuffer> &accessUnit);
+
+    void packetize(const sp<ABuffer> &accessUnit, int64_t timeUs);
+};
+
+
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/Packetizer.h b/host/frontend/gcastv2/webrtc/include/webrtc/Packetizer.h
new file mode 100644
index 0000000..d04ca0a
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/Packetizer.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <utils/Errors.h>
+#include <stdint.h>
+
+#include <memory>
+#include <vector>
+
+struct RTPSender;
+
+struct Packetizer {
+    explicit Packetizer() = default;
+    virtual ~Packetizer() = default;
+
+    virtual void run() = 0;
+    virtual uint32_t rtpNow() const = 0;
+    virtual android::status_t requestIDRFrame() = 0;
+
+    void queueRTPDatagram(std::vector<uint8_t> *packet);
+
+    void addSender(std::shared_ptr<RTPSender> sender);
+
+private:
+    std::vector<std::weak_ptr<RTPSender>> mSenders;
+};
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/RTPSender.h b/host/frontend/gcastv2/webrtc/include/webrtc/RTPSender.h
new file mode 100644
index 0000000..fefa57b
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/RTPSender.h
@@ -0,0 +1,82 @@
+#pragma once
+
+#include "Packetizer.h"
+
+#include <https/RunLoop.h>
+
+#include <memory>
+#include <optional>
+#include <unordered_map>
+#include <vector>
+
+struct RTPSocketHandler;
+
+struct RTPSender : public std::enable_shared_from_this<RTPSender> {
+
+    explicit RTPSender(
+            std::shared_ptr<RunLoop> runLoop,
+            RTPSocketHandler *parent,
+            std::shared_ptr<Packetizer> videoPacketizer,
+            std::shared_ptr<Packetizer> audioPacketizer);
+
+    void addSource(uint32_t ssrc);
+
+    void addRetransInfo(
+            uint32_t ssrc, uint8_t PT, uint32_t retransSSRC, uint8_t retransPT);
+
+    int injectRTCP(uint8_t *data, size_t size);
+    void queueRTPDatagram(std::vector<uint8_t> *packet);
+
+    void run();
+
+    void requestIDRFrame();
+
+private:
+    struct SourceInfo {
+        explicit SourceInfo()
+            : mNumPacketsSent(0),
+              mNumBytesSent(0) {
+        }
+
+        size_t mNumPacketsSent;
+        size_t mNumBytesSent;
+
+        // (ssrc, PT) by PT.
+        std::unordered_map<uint8_t, std::pair<uint32_t, uint8_t>> mRetrans;
+
+        std::deque<std::vector<uint8_t>> mRecentPackets;
+    };
+
+    std::shared_ptr<RunLoop> mRunLoop;
+    RTPSocketHandler *mParent;
+
+    // Sources by ssrc.
+    std::unordered_map<uint32_t, SourceInfo> mSources;
+
+    std::shared_ptr<Packetizer> mVideoPacketizer;
+    std::shared_ptr<Packetizer> mAudioPacketizer;
+
+    void appendSR(std::vector<uint8_t> *buffer, uint32_t localSSRC);
+    void appendSDES(std::vector<uint8_t> *buffer, uint32_t localSSRC);
+
+    void queueSR(uint32_t localSSRC);
+    void sendSR(uint32_t localSSRC);
+
+    void queueDLRR(
+            uint32_t localSSRC,
+            uint32_t remoteSSRC,
+            uint32_t ntpHi,
+            uint32_t ntpLo);
+
+    void appendDLRR(
+            std::vector<uint8_t> *buffer,
+            uint32_t localSSRC,
+            uint32_t remoteSSRC,
+            uint32_t ntpHi,
+            uint32_t ntpLo);
+
+    int processRTCP(const uint8_t *data, size_t size);
+
+    void retransmitPackets(uint32_t localSSRC, uint16_t PID, uint16_t BLP);
+};
+
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/RTPSession.h b/host/frontend/gcastv2/webrtc/include/webrtc/RTPSession.h
new file mode 100644
index 0000000..f37321b
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/RTPSession.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include <https/RunLoop.h>
+
+#include <memory>
+#include <optional>
+#include <string_view>
+
+#include <netinet/in.h>
+#include <openssl/ssl.h>
+
+struct RTPSession : std::enable_shared_from_this<RTPSession> {
+    explicit RTPSession(
+            std::string_view localUFrag,
+            std::string_view localPassword,
+            std::shared_ptr<X509> localCertificate,
+            std::shared_ptr<EVP_PKEY> localKey);
+
+    bool isActive() const;
+    void setIsActive();
+
+    void setRemoteParams(
+            std::string_view remoteUFrag,
+            std::string_view remotePassword,
+            std::string_view remoteFingerprint);
+
+    std::string localUFrag() const;
+    std::string localPassword() const;
+    std::shared_ptr<X509> localCertificate() const;
+    std::shared_ptr<EVP_PKEY> localKey() const;
+    std::string localFingerprint() const;
+
+    std::string remoteUFrag() const;
+    std::string remotePassword() const;
+    std::string remoteFingerprint() const;
+
+    bool hasRemoteAddress() const;
+    sockaddr_storage remoteAddress() const;
+    void setRemoteAddress(const sockaddr_storage &remoteAddr);
+
+    void schedulePing(
+            std::shared_ptr<RunLoop> runLoop,
+            RunLoop::AsyncFunction cb,
+            std::chrono::steady_clock::duration delay);
+
+private:
+    std::string mLocalUFrag;
+    std::string mLocalPassword;
+    std::shared_ptr<X509> mLocalCertificate;
+    std::shared_ptr<EVP_PKEY> mLocalKey;
+
+    std::optional<std::string> mRemoteUFrag;
+    std::optional<std::string> mRemotePassword;
+    std::optional<std::string> mRemoteFingerprint;
+    std::optional<sockaddr_storage> mRemoteAddr;
+
+    RunLoop::Token mPingToken;
+
+    bool mIsActive;
+};
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/RTPSocketHandler.h b/host/frontend/gcastv2/webrtc/include/webrtc/RTPSocketHandler.h
new file mode 100644
index 0000000..1933ab5
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/RTPSocketHandler.h
@@ -0,0 +1,93 @@
+#pragma once
+
+#include <https/BufferedSocket.h>
+#include <https/RunLoop.h>
+#include <webrtc/DTLS.h>
+#include <webrtc/RTPSender.h>
+#include <webrtc/RTPSession.h>
+#include <webrtc/SCTPHandler.h>
+#include <webrtc/ServerState.h>
+#include <webrtc/STUNMessage.h>
+
+#include <memory>
+#include <string_view>
+#include <vector>
+
+struct MyWebSocketHandler;
+
+struct RTPSocketHandler
+    : public std::enable_shared_from_this<RTPSocketHandler> {
+
+    static constexpr size_t kMaxUDPPayloadSize = 1536;
+
+    static constexpr uint32_t TRACK_VIDEO = 1;
+    static constexpr uint32_t TRACK_AUDIO = 2;
+    static constexpr uint32_t TRACK_DATA  = 4;
+
+    explicit RTPSocketHandler(
+            std::shared_ptr<RunLoop> runLoop,
+            std::shared_ptr<ServerState> serverState,
+            int domain,
+            uint16_t port,
+            uint32_t trackMask,
+            std::shared_ptr<RTPSession> session);
+
+    uint16_t getLocalPort() const;
+    std::string getLocalUFrag() const;
+    std::string getLocalIPString() const;
+
+    void run();
+
+    void queueDatagram(
+            const sockaddr_storage &addr, const void *data, size_t size);
+
+    void queueRTCPDatagram(const void *data, size_t size);
+    void queueRTPDatagram(const void *data, size_t size);
+
+    void notifyDTLSConnected();
+
+private:
+    struct Datagram {
+        explicit Datagram(
+                const sockaddr_storage &addr, const void *data, size_t size);
+
+        const void *data() const;
+        size_t size() const;
+
+        const sockaddr_storage &remoteAddress() const;
+
+    private:
+        std::vector<uint8_t> mData;
+        sockaddr_storage mAddr;
+    };
+
+    std::shared_ptr<RunLoop> mRunLoop;
+    std::shared_ptr<ServerState> mServerState;
+    uint16_t mLocalPort;
+    uint32_t mTrackMask;
+    std::shared_ptr<RTPSession> mSession;
+
+    std::shared_ptr<BufferedSocket> mSocket;
+    std::shared_ptr<DTLS> mDTLS;
+    std::shared_ptr<SCTPHandler> mSCTPHandler;
+
+    std::deque<std::shared_ptr<Datagram>> mOutQueue;
+    bool mSendPending;
+    bool mDTLSConnected;
+
+    std::shared_ptr<RTPSender> mRTPSender;
+
+    void onReceive();
+    void onDTLSReceive(const uint8_t *data, size_t size);
+
+    void pingRemote(std::shared_ptr<RTPSession> session);
+
+    bool matchesSession(const STUNMessage &msg) const;
+
+    void scheduleDrainOutQueue();
+    void drainOutQueue();
+
+    int onSRTPReceive(uint8_t *data, size_t size);
+};
+
+
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/SCTPHandler.h b/host/frontend/gcastv2/webrtc/include/webrtc/SCTPHandler.h
new file mode 100644
index 0000000..eabad73
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/SCTPHandler.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <webrtc/DTLS.h>
+
+#include <https/RunLoop.h>
+
+#include <memory>
+
+struct SCTPHandler : public std::enable_shared_from_this<SCTPHandler> {
+    explicit SCTPHandler(
+            std::shared_ptr<RunLoop> runLoop,
+            std::shared_ptr<DTLS> dtls);
+
+    void run();
+
+    int inject(uint8_t *data, size_t size);
+
+private:
+    std::shared_ptr<RunLoop> mRunLoop;
+    std::shared_ptr<DTLS> mDTLS;
+
+    uint32_t mInitiateTag;
+    uint32_t mSendingTSN;
+    bool mSentGreeting;
+
+    int processChunk(
+            uint16_t srcPort,
+            const uint8_t *data,
+            size_t size,
+            bool firstChunk,
+            bool lastChunk);
+
+    static uint32_t crc32c(const uint8_t *data, size_t size);
+
+    void onSendGreeting(uint16_t srcPort, size_t index);
+};
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/SDP.h b/host/frontend/gcastv2/webrtc/include/webrtc/SDP.h
new file mode 100644
index 0000000..c13bf61
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/SDP.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+struct SDP {
+    explicit SDP();
+
+    int initCheck() const;
+
+    void clear();
+    int setTo(const std::string &data);
+
+    // Section 0 is reserved for top-level attributes, section indices >= 1
+    // correspond to each media section starting with an "m=" line.
+    size_t countSections() const;
+
+    std::vector<std::string>::const_iterator section_begin(
+            size_t section) const;
+
+    std::vector<std::string>::const_iterator section_end(
+            size_t section) const;
+
+    struct SectionEditor {
+        ~SectionEditor();
+
+        SectionEditor &operator<<(std::string_view s);
+
+        void commit();
+
+    private:
+        friend struct SDP;
+
+        explicit SectionEditor(SDP *sdp, size_t section);
+
+        SDP *mSDP;
+        size_t mSection;
+
+        std::string mBuffer;
+    };
+
+    SectionEditor createSection();
+    SectionEditor appendToSection(size_t section);
+
+    static void Test();
+
+private:
+    int mInitCheck;
+    std::vector<std::string> mLines;
+
+    std::vector<size_t> mLineIndexBySection;
+
+    bool mNewSectionEditorActive;
+
+    void getSectionRange(
+            size_t section,
+            size_t *lineStartIndex,
+            size_t *lineStopIndex) const;
+
+    void commitSectionEdit(
+            size_t section, const std::vector<std::string> &lines);
+};
+
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/STUNMessage.h b/host/frontend/gcastv2/webrtc/include/webrtc/STUNMessage.h
new file mode 100644
index 0000000..08a0731
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/STUNMessage.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include <cstdint>
+#include <optional>
+#include <string_view>
+#include <vector>
+
+struct STUNMessage {
+    explicit STUNMessage(uint16_t type, const uint8_t transactionID[12]);
+    explicit STUNMessage(const void *data, size_t size);
+
+    bool isValid() const;
+
+    uint16_t type() const;
+
+    void addAttribute(uint16_t type) {
+        addAttribute(type, nullptr, 0);
+    }
+
+    void addAttribute(uint16_t type, const void *data, size_t size);
+    void addMessageIntegrityAttribute(std::string_view password);
+    void addFingerprint();
+
+    bool findAttribute(uint16_t type, const void **data, size_t *size) const;
+
+    const uint8_t *data();
+    size_t size() const;
+
+    void dump(std::optional<std::string_view> password = std::nullopt) const;
+
+private:
+    bool mIsValid;
+    std::vector<uint8_t> mData;
+    bool mAddedMessageIntegrity;
+
+    void validate();
+
+    bool verifyMessageIntegrity(size_t offset, std::string_view password) const;
+    bool verifyFingerprint(size_t offset) const;
+};
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/ServerState.h b/host/frontend/gcastv2/webrtc/include/webrtc/ServerState.h
new file mode 100644
index 0000000..dfeb66e
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/ServerState.h
@@ -0,0 +1,91 @@
+#pragma once
+
+#include "Packetizer.h"
+
+#include <https/RunLoop.h>
+
+#ifdef TARGET_ANDROID
+#include <source/HostToGuestComms.h>
+#elif defined(TARGET_ANDROID_DEVICE)
+#include <media/stagefright/foundation/ALooper.h>
+#include <platform/MyContext.h>
+#else
+#error "Either TARGET_ANDROID or TARGET_ANDROID_DEVICE must be defined."
+#endif
+
+#include <source/StreamingSink.h>
+#include <source/StreamingSource.h>
+
+#include <memory>
+#include <mutex>
+#include <set>
+
+struct ServerState {
+    using StreamingSink = android::StreamingSink;
+
+#ifdef TARGET_ANDROID_DEVICE
+    template<class T> using sp = android::sp<T>;
+    using MyContext = android::MyContext;
+#endif
+
+    enum class VideoFormat {
+        H264,
+        VP8,
+    };
+    explicit ServerState(
+#ifdef TARGET_ANDROID_DEVICE
+            const sp<MyContext> &context,
+#endif
+            std::shared_ptr<RunLoop> runLoop,
+            VideoFormat videoFormat);
+
+    std::shared_ptr<Packetizer> getVideoPacketizer();
+    std::shared_ptr<Packetizer> getAudioPacketizer();
+    std::shared_ptr<StreamingSink> getTouchSink();
+
+    VideoFormat videoFormat() const { return mVideoFormat; }
+
+    size_t acquireHandlerId();
+    void releaseHandlerId(size_t id);
+
+    uint16_t acquirePort();
+    void releasePort(uint16_t port);
+
+private:
+    using StreamingSource = android::StreamingSource;
+
+#ifdef TARGET_ANDROID_DEVICE
+    sp<MyContext> mContext;
+#endif
+
+    std::shared_ptr<RunLoop> mRunLoop;
+
+    VideoFormat mVideoFormat;
+
+    std::weak_ptr<Packetizer> mVideoPacketizer;
+    std::weak_ptr<Packetizer> mAudioPacketizer;
+
+    std::shared_ptr<StreamingSource> mFrameBufferSource;
+
+    std::shared_ptr<StreamingSource> mAudioSource;
+
+#ifdef TARGET_ANDROID
+    std::shared_ptr<HostToGuestComms> mHostToGuestComms;
+    std::shared_ptr<HostToGuestComms> mFrameBufferComms;
+    std::shared_ptr<HostToGuestComms> mAudioComms;
+#else
+    using ALooper = android::ALooper;
+    sp<ALooper> mLooper, mAudioLooper;
+#endif
+
+    std::shared_ptr<StreamingSink> mTouchSink;
+
+    std::set<size_t> mAllocatedHandlerIds;
+
+    std::mutex mPortLock;
+    std::set<uint16_t> mAvailablePorts;
+
+#ifdef TARGET_ANDROID
+    void changeResolution(int32_t width, int32_t height, int32_t densityDpi);
+#endif
+};
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/VP8Packetizer.h b/host/frontend/gcastv2/webrtc/include/webrtc/VP8Packetizer.h
new file mode 100644
index 0000000..5877851
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/VP8Packetizer.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "Packetizer.h"
+
+#include <https/RunLoop.h>
+#include <media/stagefright/foundation/ABuffer.h>
+#include <media/stagefright/NuMediaExtractor.h>
+#include <source/StreamingSource.h>
+
+#include <memory>
+
+struct VP8Packetizer
+    : public Packetizer, public std::enable_shared_from_this<VP8Packetizer> {
+
+    using StreamingSource = android::StreamingSource;
+
+    explicit VP8Packetizer(
+            std::shared_ptr<RunLoop> runLoop,
+            std::shared_ptr<StreamingSource> frameBufferSource);
+
+    ~VP8Packetizer() override;
+
+    void run() override;
+    uint32_t rtpNow() const override;
+    android::status_t requestIDRFrame() override;
+
+private:
+    template<class T> using sp = android::sp<T>;
+    using ABuffer = android::ABuffer;
+
+    std::shared_ptr<RunLoop> mRunLoop;
+
+    std::shared_ptr<StreamingSource> mFrameBufferSource;
+
+    size_t mNumSamplesRead;
+
+    std::chrono::time_point<std::chrono::steady_clock> mStartTimeReal;
+    int64_t mStartTimeMedia;
+
+    void onFrame(const sp<ABuffer> &accessUnit);
+
+    void packetize(const sp<ABuffer> &accessUnit, int64_t timeUs);
+};
+
diff --git a/host/frontend/gcastv2/webrtc/index.html b/host/frontend/gcastv2/webrtc/index.html
new file mode 100644
index 0000000..fe4cb3c
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/index.html
@@ -0,0 +1,27 @@
+<html>
+    <head>
+        <title>My WebRTC Playground</title>
+
+        <link rel="stylesheet" type="text/css" href="style.css" >
+    </head>
+
+    <body>
+        <button id="receiveButton">Receive Media</button>
+        <hr>
+        <section class="noscroll">
+            <div class="one" >
+                <video id="video" autoplay controls width="360" height="720" style="touch-action:none" ></video>
+            </div>
+
+            <div class="two" >
+                <textarea id="logcat" rows="55" cols="120" readonly >
+                </textarea>
+            </div>
+        </section>
+
+        <script src="js/receive.js"></script>
+        <script src="js/logcat.js"></script>
+    </body>
+
+</html>
+
diff --git a/host/frontend/gcastv2/webrtc/js/logcat.js b/host/frontend/gcastv2/webrtc/js/logcat.js
new file mode 100644
index 0000000..aae94bc
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/js/logcat.js
@@ -0,0 +1,163 @@
+let adb_ws;
+let logcat = document.getElementById('logcat');
+
+let utf8Encoder = new TextEncoder();
+let utf8Decoder = new TextDecoder();
+
+const A_CNXN = 0x4e584e43;
+const A_OPEN = 0x4e45504f;
+const A_WRTE = 0x45545257;
+const A_OKAY = 0x59414b4f;
+
+const kLocalChannelId = 666;
+
+function setU32LE(array, offset, x) {
+    array[offset] = x & 0xff;
+    array[offset + 1] = (x >> 8) & 0xff;
+    array[offset + 2] = (x >> 16) & 0xff;
+    array[offset + 3] = x >> 24;
+}
+
+function getU32LE(array, offset) {
+    let x = array[offset]
+        | (array[offset + 1] << 8)
+        | (array[offset + 2] << 16)
+        | (array[offset + 3] << 24);
+
+    return x >>> 0;  // convert signed to unsigned if necessary.
+}
+
+function computeChecksum(array) {
+    let sum = 0;
+    let i;
+    for (i = 0; i < array.length; ++i) {
+        sum = ((sum + array[i]) & 0xffffffff) >>> 0;
+    }
+
+    return sum;
+}
+
+function createAdbMessage(command, arg0, arg1, payload) {
+    let arrayBuffer = new ArrayBuffer(24 + payload.length);
+    let array = new Uint8Array(arrayBuffer);
+    setU32LE(array, 0, command);
+    setU32LE(array, 4, arg0);
+    setU32LE(array, 8, arg1);
+    setU32LE(array, 12, payload.length);
+    setU32LE(array, 16, computeChecksum(payload));
+    setU32LE(array, 20, command ^ 0xffffffff);
+    array.set(payload, 24);
+
+    return arrayBuffer;
+}
+
+function adbOpenConnection() {
+    let systemIdentity = utf8Encoder.encode("Cray_II:1234:whatever");
+
+    let arrayBuffer = createAdbMessage(
+        A_CNXN, 0x1000000, 256 * 1024, systemIdentity);
+
+    adb_ws.send(arrayBuffer);
+}
+
+function adbOpenChannel() {
+    let destination = utf8Encoder.encode("shell:logcat");
+
+    let arrayBuffer = createAdbMessage(A_OPEN, kLocalChannelId, 0, destination);
+    adb_ws.send(arrayBuffer);
+}
+
+function adbSendOkay(remoteId) {
+    let payload = new Uint8Array(0);
+
+    let arrayBuffer = createAdbMessage(
+        A_OKAY, kLocalChannelId, remoteId, payload);
+
+    adb_ws.send(arrayBuffer);
+}
+
+function adbOnMessage(ev) {
+    // console.log("adb_ws: onmessage (" + ev.data.byteLength + " bytes)");
+
+    let arrayBuffer = ev.data;
+    let array = new Uint8Array(arrayBuffer);
+
+    if (array.length < 24) {
+        console.log("adb message too short.");
+        return;
+    }
+
+    let command = getU32LE(array, 0);
+    let magic = getU32LE(array, 20);
+
+    if (command != ((magic ^ 0xffffffff) >>> 0)) {
+        console.log("command = " + command + ", magic = " + magic);
+        console.log("adb message command vs magic failed.");
+        return;
+    }
+
+    let payloadLength = getU32LE(array, 12);
+
+    if (array.length != 24 + payloadLength) {
+        console.log("adb message length mismatch.");
+        return;
+    }
+
+    let payloadChecksum = getU32LE(array, 16);
+    let checksum = computeChecksum(array.slice(24));
+
+    if (payloadChecksum != checksum) {
+        console.log("adb message checksum mismatch.");
+        return;
+    }
+
+    switch (command) {
+        case A_CNXN:
+        {
+            console.log("connected.");
+
+            adbOpenChannel();
+            break;
+        }
+
+        case A_OKAY:
+        {
+            let remoteId = getU32LE(array, 4);
+            console.log("channel created w/ remoteId " + remoteId);
+            break;
+        }
+
+        case A_WRTE:
+        {
+            let payloadText = utf8Decoder.decode(array.slice(24));
+
+            logcat.value += payloadText;
+
+            // Scroll to bottom
+            logcat.scrollTop = logcat.scrollHeight;
+
+            let remoteId = getU32LE(array, 4);
+            adbSendOkay(remoteId);
+            break;
+        }
+    }
+}
+
+function init_logcat() {
+    const wsProtocol = (location.protocol == "http:") ? "ws:" : "wss:";
+
+    adb_ws = new WebSocket(
+        wsProtocol + "//" + location.host + "/control_adb");
+
+    adb_ws.binaryType = "arraybuffer";
+
+    adb_ws.onopen = function() {
+        console.log("adb_ws: onopen");
+
+        adbOpenConnection();
+
+        logcat.style.display = "initial";
+    };
+    adb_ws.onmessage = adbOnMessage;
+}
+
diff --git a/host/frontend/gcastv2/webrtc/js/receive.js b/host/frontend/gcastv2/webrtc/js/receive.js
new file mode 100644
index 0000000..a86ba93
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/js/receive.js
@@ -0,0 +1,442 @@
+'use strict';
+
+const receiveButton = document.getElementById('receiveButton');
+receiveButton.addEventListener('click', onReceive);
+
+const videoElement = document.getElementById('video');
+
+videoElement.addEventListener("click", onInitialClick);
+
+function onInitialClick(e) {
+    // This stupid thing makes sure that we disable controls after the first click...
+    // Why not just disable controls altogether you ask? Because then audio won't play
+    // because these days user-interaction is required to enable audio playback...
+    console.log("onInitialClick");
+
+    videoElement.controls = false;
+    videoElement.removeEventListener("click", onInitialClick);
+}
+
+let pc1;
+let pc2;
+
+let dataChannel;
+
+let ws;
+
+let offerResolve;
+let iceCandidateResolve;
+
+let videoStream;
+
+let mouseIsDown = false;
+
+const run_locally = false;
+
+const is_chrome = navigator.userAgent.indexOf("Chrome") !== -1;
+
+function handleDataChannelStatusChange(event) {
+    console.log('handleDataChannelStatusChange state=' + dataChannel.readyState);
+
+    if (dataChannel.readyState == "open") {
+        dataChannel.send("Hello, world!");
+    }
+}
+
+function handleDataChannelMessage(event) {
+    console.log('handleDataChannelMessage data="' + event.data + '"');
+}
+
+async function onReceive() {
+    console.log('onReceive');
+    receiveButton.disabled = true;
+
+    init_logcat();
+
+    if (!run_locally) {
+        const wsProtocol = (location.protocol == "http:") ? "ws:" : "wss:";
+
+        ws = new WebSocket(wsProtocol + "//" + location.host + "/control");
+        ws.onopen = function() {
+            console.log("onopen");
+            ws.send('{\r\n'
+                +     '"type": "greeting",\r\n'
+                +     '"message": "Hello, world!",\r\n'
+                +     '"path": "' + location.pathname + location.search + '"\r\n'
+                +   '}');
+        };
+        ws.onmessage = function(e) {
+            console.log("onmessage " + e.data);
+
+            let data = JSON.parse(e.data);
+            if (data.type == "hello") {
+                kickoff();
+            } else if (data.type == "offer" && offerResolve) {
+                offerResolve(data.sdp);
+                offerResolve = undefined;
+            } else if (data.type == "ice-candidate" && iceCandidateResolve) {
+                iceCandidateResolve(data);
+
+                iceCandidateResolve = undefined;
+            }
+        };
+    }
+
+    if (run_locally) {
+        pc1 = new RTCPeerConnection();
+        console.log('got pc1=' + pc1);
+
+        pc1.addEventListener(
+            'icecandidate', e => onIceCandidate(pc1, e));
+
+        pc1.addEventListener(
+            'iceconnectionstatechange', e => onIceStateChange(pc1, e));
+
+        const stream =
+            await navigator.mediaDevices.getUserMedia(
+                {
+                    audio: true,
+                    video: { width: 1280, height: 720 },
+                });
+
+        stream.getTracks().forEach(track => pc1.addTrack(track, stream));
+    }
+
+    pc2 = new RTCPeerConnection();
+    console.log('got pc2=' + pc2);
+
+    pc2.addEventListener(
+        'icecandidate', e => onIceCandidate(pc2, e));
+
+    pc2.addEventListener(
+        'iceconnectionstatechange', e => onIceStateChange(pc2, e));
+
+    pc2.addEventListener(
+        'connectionstatechange', e => {
+        console.log("connection state = " + pc2.connectionState);
+    });
+
+    pc2.addEventListener('track', onGotRemoteStream);
+
+    dataChannel = pc2.createDataChannel("data-channel");
+    dataChannel.onopen = handleDataChannelStatusChange;
+    dataChannel.onclose = handleDataChannelStatusChange;
+    dataChannel.onmessage = handleDataChannelMessage;
+
+    if (run_locally) {
+        kickoff();
+    }
+}
+
+async function kickoff() {
+    console.log('createOffer start');
+
+    try {
+        var offer;
+
+        if (run_locally) {
+            const offerOptions = {
+                offerToReceiveAudio: 0,
+                offerToReceiveVideo: 1
+            };
+            offer = await pc1.createOffer(offerOptions);
+        } else {
+            offer = await getWsOffer();
+        }
+        await onCreateOfferSuccess(offer);
+    } catch (e) {
+        console.log('createOffer FAILED ');
+    }
+}
+
+async function onCreateOfferSuccess(desc) {
+    console.log(`Offer ${desc.sdp}`);
+
+    try {
+        pc2.setRemoteDescription(desc);
+    } catch (e) {
+        console.log('setRemoteDescription pc2 FAILED');
+        return;
+    }
+
+    console.log('setRemoteDescription pc2 successful.');
+
+    try {
+        if (run_locally) {
+            await pc1.setLocalDescription(desc);
+        } else {
+            setWsLocalDescription(desc);
+        }
+    } catch (e) {
+        console.log('setLocalDescription pc1 FAILED');
+        return;
+    }
+
+    console.log('setLocalDescription pc1 successful.');
+
+    try {
+        const answer = await pc2.createAnswer();
+
+        await onCreateAnswerSuccess(answer);
+    } catch (e) {
+        console.log('createAnswer FAILED');
+    }
+}
+
+function setWsRemoteDescription(desc) {
+    ws.send('{\r\n'
+        +     '"type": "set-remote-desc",\r\n'
+        +     '"sdp": "' + desc.sdp + '"\r\n'
+        +   '}');
+}
+
+function setWsLocalDescription(desc) {
+    ws.send('{\r\n'
+        +     '"type": "set-local-desc",\r\n'
+        +     '"sdp": "' + desc.sdp + '"\r\n'
+        +   '}');
+}
+
+async function getWsOffer() {
+    const offerPromise = new Promise(function(resolve, reject) {
+        offerResolve = resolve;
+    });
+
+    ws.send('{\r\n'
+        +     '"type": "request-offer",\r\n'
+        +     (is_chrome ? '"is_chrome": 1\r\n'
+                         : '"is_chrome": 0\r\n')
+        +   '}');
+
+    const sdp = await offerPromise;
+
+    return { type: "offer", sdp: sdp };
+}
+
+async function getWsIceCandidate(mid) {
+    console.log("getWsIceCandidate (mid=" + mid + ")");
+
+    const answerPromise = new Promise(function(resolve, reject) {
+        iceCandidateResolve = resolve;
+    });
+
+    ws.send('{\r\n'
+        +     '"type": "get-ice-candidate",\r\n'
+        +     '"mid": ' + mid + ',\r\n'
+        +   '}');
+
+    const replyInfo = await answerPromise;
+
+    console.log("got replyInfo '" + replyInfo + "'");
+
+    if (replyInfo == undefined || replyInfo.candidate == undefined) {
+        return null;
+    }
+
+    const replyCandidate = replyInfo.candidate;
+    const mlineIndex = replyInfo.mlineIndex;
+
+    let result;
+    try {
+        result = new RTCIceCandidate(
+            {
+                sdpMid: mid,
+                sdpMLineIndex: mlineIndex,
+                candidate: replyCandidate
+            });
+    }
+    catch (e) {
+        console.log("new RTCIceCandidate FAILED. " + e);
+        return undefined;
+    }
+
+    console.log("got result " + result);
+
+    return result;
+}
+
+async function addRemoteIceCandidate(mid) {
+    const candidate = await getWsIceCandidate(mid);
+
+    if (!candidate) {
+        return false;
+    }
+
+    try {
+        await pc2.addIceCandidate(candidate);
+    } catch (e) {
+        console.log("addIceCandidate pc2 FAILED w/ " + e);
+        return false;
+    }
+
+    console.log("addIceCandidate pc2 successful. (mid="
+        + mid + ", mlineIndex=" + candidate.sdpMLineIndex + ")");
+
+    return true;
+}
+
+async function onCreateAnswerSuccess(desc) {
+    console.log(`Answer ${desc.sdp}`);
+
+    try {
+        await pc2.setLocalDescription(desc);
+    } catch (e) {
+        console.log('setLocalDescription pc2 FAILED ' + e);
+        return;
+    }
+
+    console.log('setLocalDescription pc2 successful.');
+
+    try {
+        if (run_locally) {
+            await pc1.setRemoteDescription(desc);
+        } else {
+            setWsRemoteDescription(desc);
+        }
+    } catch (e) {
+        console.log('setRemoteDescription pc1 FAILED');
+        return;
+    }
+
+    console.log('setRemoteDescription pc1 successful.');
+
+    if (!run_locally) {
+        if (!await addRemoteIceCandidate(0)) {
+            return;
+        }
+        await addRemoteIceCandidate(1);
+        await addRemoteIceCandidate(2);  // XXX
+    }
+}
+
+function getPcName(pc) {
+    return ((pc == pc2) ? "pc2" : "pc1");
+}
+
+async function onIceCandidate(pc, e) {
+    console.log(
+        getPcName(pc)
+        + ' onIceCandidate '
+        + (e.candidate ? ('"' + e.candidate.candidate + '"') : '(null)')
+        + " "
+        + (e.candidate ? ('sdmMid: ' + e.candidate.sdpMid) : '(null)')
+        + " "
+        + (e.candidate ? ('sdpMLineIndex: ' + e.candidate.sdpMLineIndex) : '(null)'));
+
+    if (!e.candidate) {
+        return;
+    }
+
+    let other_pc = (pc == pc2) ? pc1 : pc2;
+
+    if (other_pc) {
+        try {
+            await other_pc.addIceCandidate(e.candidate);
+        } catch (e) {
+            console.log('addIceCandidate FAILED ' + e);
+            return;
+        }
+
+        console.log('addIceCandidate successful.');
+    }
+}
+
+async function onIceStateChange(pc, e) {
+    console.log(
+        'onIceStateChange ' + getPcName(pc) + " '" + pc.iceConnectionState + "'");
+
+    if (pc.iceConnectionState == "connected") {
+        videoElement.srcObject = videoStream;
+
+        startMouseTracking()
+    } else if (pc.iceConnectionState == "disconnected") {
+        stopMouseTracking()
+    }
+}
+
+async function onGotRemoteStream(e) {
+    console.log('onGotRemoteStream ' + e);
+
+    const track = e.track;
+
+    console.log('track = ' + track);
+    console.log('track.kind = ' + track.kind);
+    console.log('track.readyState = ' + track.readyState);
+    console.log('track.enabled = ' + track.enabled);
+
+    if (track.kind == "video") {
+        videoStream = e.streams[0];
+    }
+}
+
+function startMouseTracking() {
+    if (window.PointerEvent) {
+        videoElement.addEventListener("pointerdown", onStartDrag);
+        videoElement.addEventListener("pointermove", onContinueDrag);
+        videoElement.addEventListener("pointerup", onEndDrag);
+    } else if (window.TouchEvent) {
+        videoElement.addEventListener("touchstart", onStartDrag);
+        videoElement.addEventListener("touchmove", onContinueDrag);
+        videoElement.addEventListener("touchend", onEndDrag);
+    } else if (window.MouseEvent) {
+        videoElement.addEventListener("mousedown", onStartDrag);
+        videoElement.addEventListener("mousemove", onContinueDrag);
+        videoElement.addEventListener("mouseup", onEndDrag);
+    }
+}
+
+function stopMouseTracking() {
+    if (window.PointerEvent) {
+        videoElement.removeEventListener("pointerdown", onStartDrag);
+        videoElement.removeEventListener("pointermove", onContinueDrag);
+        videoElement.removeEventListener("pointerup", onEndDrag);
+    } else if (window.TouchEvent) {
+        videoElement.removeEventListener("touchstart", onStartDrag);
+        videoElement.removeEventListener("touchmove", onContinueDrag);
+        videoElement.removeEventListener("touchend", onEndDrag);
+    } else if (window.MouseEvent) {
+        videoElement.removeEventListener("mousedown", onStartDrag);
+        videoElement.removeEventListener("mousemove", onContinueDrag);
+        videoElement.removeEventListener("mouseup", onEndDrag);
+    }
+}
+
+function onStartDrag(e) {
+    e.preventDefault();
+
+    // console.log("mousedown at " + e.pageX + " / " + e.pageY);
+    mouseIsDown = true;
+
+    sendMouseUpdate(true, e);
+}
+
+function onEndDrag(e) {
+    e.preventDefault();
+
+    // console.log("mouseup at " + e.pageX + " / " + e.pageY);
+    mouseIsDown = false;
+
+    sendMouseUpdate(false, e);
+}
+
+function onContinueDrag(e) {
+    e.preventDefault();
+
+    // console.log("mousemove at " + e.pageX + " / " + e.pageY + ", down=" + mouseIsDown);
+    if (mouseIsDown) {
+        sendMouseUpdate(true, e);
+    }
+}
+
+function sendMouseUpdate(down, e) {
+    const x = e.pageX - 7;
+    const y = e.pageY - 46;
+
+    ws.send('{\r\n'
+        +     '"type": "set-mouse-position",\r\n'
+        +     '"down": ' + (down ? "1" : "0") + ',\r\n'
+        +     '"x": ' + Math.trunc(x) + ',\r\n'
+        +     '"y": ' + Math.trunc(y) + '\r\n'
+        +   '}');
+}
+
diff --git a/host/frontend/gcastv2/webrtc/makefile b/host/frontend/gcastv2/webrtc/makefile
new file mode 100644
index 0000000..e5d8e05
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/makefile
@@ -0,0 +1,46 @@
+LOCAL_PATH := $(PWD)
+
+all: out/webRTC
+
+include ../build/defaults
+
+TARGET := webRTC
+
+C++FLAGS := \
+    -I../https/include                  \
+    -I../new2/libandroid/include        \
+
+C++FLAGS += -O0 -g -Wall -Wextra -DTARGET_MAC=1
+
+C++FLAGS += -Wno-gnu-anonymous-struct -Wno-nested-anon-types
+C++FLAGS += -fno-rtti
+
+LDFLAGS += \
+    -framework CoreFoundation   \
+    -framework Security         \
+
+C++FLAGS += -I/usr/local/opt/openssl/include
+LDFLAGS += -L/usr/local/opt/openssl/lib -lssl -lcrypto
+
+C++FLAGS += -I/usr/local/opt/srtp/include
+LDFLAGS += -L/usr/local/opt/srtp/lib -lsrtp2
+
+STATIC_LIBS := libandroid.a libhttps.a
+
+SRCS := \
+    DTLS.cpp                            \
+    H264Assembler.cpp                   \
+    H264Packetizer.cpp                  \
+    MyWebSocketHandler.cpp              \
+    RTPReceiver.cpp                     \
+    RTPSender.cpp                       \
+    RTPSession.cpp                      \
+    RTPSocketHandler.cpp                \
+    STUNMessage.cpp                     \
+    webRTC.cpp                          \
+
+include ../build/build_executable
+include ../build/clear
+
+include ../https/local.mak
+include ../new2/libandroid/local.mak
diff --git a/host/frontend/gcastv2/webrtc/style.css b/host/frontend/gcastv2/webrtc/style.css
new file mode 100644
index 0000000..d98614d
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/style.css
@@ -0,0 +1,22 @@
+body,
+html {
+#    position: fixed;
+}
+
+.noscroll {
+    touch-action: none;
+}
+
+.one {
+    float: left;
+}
+
+.two {
+}
+
+#logcat {
+    display: none;
+    font-family: monospace;
+    padding: 10px;
+}
+
diff --git a/host/frontend/gcastv2/webrtc/webRTC.cpp b/host/frontend/gcastv2/webrtc/webRTC.cpp
new file mode 100644
index 0000000..6e38333
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/webRTC.cpp
@@ -0,0 +1,162 @@
+#include <webrtc/AdbWebSocketHandler.h>
+#include <webrtc/DTLS.h>
+#include <webrtc/MyWebSocketHandler.h>
+#include <webrtc/RTPSocketHandler.h>
+#include <webrtc/ServerState.h>
+#include <webrtc/STUNMessage.h>
+
+#include <https/HTTPServer.h>
+#include <https/PlainSocket.h>
+#include <https/RunLoop.h>
+#include <https/SafeCallbackable.h>
+#include <https/SSLSocket.h>
+#include <https/Support.h>
+#include <media/stagefright/foundation/hexdump.h>
+#include <media/stagefright/Utils.h>
+
+#include <iostream>
+#include <unordered_map>
+
+#if defined(TARGET_ANDROID)
+#include <gflags/gflags.h>
+
+DEFINE_string(
+        public_ip,
+        "0.0.0.0",
+        "Public IPv4 address of your server, a.b.c.d format");
+DEFINE_string(
+        assets_dir,
+        "webrtc",
+        "Directory with location of webpage assets.");
+DEFINE_string(
+        certs_dir,
+        "webrtc/certs",
+        "Directory to certificates.");
+
+DEFINE_int32(touch_fd, -1, "An fd to listen on for touch connections.");
+DEFINE_int32(keyboard_fd, -1, "An fd to listen on for keyboard connections.");
+
+DEFINE_string(adb, "", "Interface:port of local adb service.");
+#endif
+
+int main(int argc, char **argv) {
+#if defined(TARGET_ANDROID)
+    ::gflags::ParseCommandLineFlags(&argc, &argv, true);
+#else
+    (void)argc;
+    (void)argv;
+#endif
+
+    SSLSocket::Init();
+    DTLS::Init();
+
+    auto runLoop = RunLoop::main();
+
+    auto state = std::make_shared<ServerState>(
+            runLoop, ServerState::VideoFormat::VP8);
+
+#if 1
+    auto port = 8443;  // Change to 8080 to use plain http instead of https.
+
+    auto httpd = std::make_shared<HTTPServer>(
+            runLoop,
+            "0.0.0.0",
+            port,
+            port == 8080
+                ? ServerSocket::TransportType::TCP
+                : ServerSocket::TransportType::TLS,
+            FLAGS_certs_dir + "/server.crt",
+            FLAGS_certs_dir + "/server.key");
+
+    const std::string index_html = FLAGS_assets_dir + "/index.html";
+    const std::string receive_js = FLAGS_assets_dir + "/js/receive.js";
+    const std::string logcat_js = FLAGS_assets_dir + "/js/logcat.js";
+    const std::string style_css = FLAGS_assets_dir + "/style.css";
+
+    httpd->addStaticFile("/index.html", index_html.c_str());
+    httpd->addStaticFile("/js/receive.js", receive_js.c_str());
+    httpd->addStaticFile("/js/logcat.js", logcat_js.c_str());
+    httpd->addStaticFile("/style.css", style_css.c_str());
+
+    httpd->addWebSocketHandlerFactory(
+            "/control",
+            [runLoop, state]{
+                auto id = state->acquireHandlerId();
+
+                auto handler =
+                    std::make_shared<MyWebSocketHandler>(runLoop, state, id);
+
+                return std::make_pair(0 /* OK */, handler);
+            });
+
+#if defined(TARGET_ANDROID)
+    if (!FLAGS_adb.empty()) {
+        httpd->addWebSocketHandlerFactory(
+                "/control_adb",
+                [runLoop]{
+                    auto handler = std::make_shared<AdbWebSocketHandler>(
+                            runLoop, FLAGS_adb);
+
+                    handler->run();
+
+                    return std::make_pair(0 /* OK */, handler);
+                });
+    }
+#endif
+
+    httpd->run();
+#else
+    uint16_t receiverPort = 63843;
+    std::string receiverUFrag = "N1NB";
+    std::string receiverPassword = "deadbeef";
+
+    uint16_t senderPort = 63844;
+    std::string senderUFrag = "ABCD";
+    std::string senderPassword = "wooops";
+
+    auto sender = std::make_shared<RTPSocketHandler>(
+            runLoop,
+            RTPSocketHandler::Mode::CONTROLLER,
+            AF_INET,
+            senderPort,
+            false /* isChrome */);
+
+    sender->addSession(
+            senderUFrag,
+            senderPassword,
+            receiverUFrag,
+            receiverPassword);
+
+    sockaddr_in addr;
+    memset(&addr, 0, sizeof(addr));
+    addr.sin_family = AF_INET;
+    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+    addr.sin_port = htons(senderPort);
+
+    sockaddr_storage senderAddr;
+    memcpy(&senderAddr, &addr, sizeof(addr));
+
+    auto receiver = std::make_shared<RTPSocketHandler>(
+            runLoop,
+            RTPSocketHandler::Mode::CONTROLLEE,
+            AF_INET,
+            receiverPort,
+            false /* isChrome */);
+
+    receiver->addSession(
+            receiverUFrag,
+            receiverPassword,
+            senderUFrag,
+            senderPassword,
+            senderAddr);
+
+    sender->run();
+    receiver->run();
+
+    receiver->kick();
+#endif
+
+    runLoop->run();
+
+    return 0;
+}