Add webrtc streamer

Bug: 141887532
Test: locally
Change-Id: I2a0d927470ba1f68cc94b94987a0e0de8b74333e
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;
+}