From 4f06c30c0598df9c316050cad9c7aaaf52a8d7d2 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Tue, 11 Aug 2026 14:51:56 +0700 Subject: [PATCH] spike: add addTrack + TrackWrap to libdatachannel-min binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose rtc::Track with send(binary) for raw RTP — verified: - SDP from addTrack(audio)+addTrack(video) has m=audio (opus 120) and m=video (H264 101 + H265/VP8/VP9/AV1 + RTX) - libdatachannel Track::send() sends RAW RTP/RTCP when no media handler is set (verified in src/track.cpp impl::Track::outgoing) — so RTP packetization can live in pure JS, keeping the binding minimal Also fix: Track class was missing from InitAll exports (crash on TrackWrap::NewInstance — null FunctionReference). --- .../native/libdatachannel-min/binding.cpp | 99 ++++++++++++++++++- .../native/libdatachannel-min/test-track.js | 33 +++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 services/discord-gateway/native/libdatachannel-min/test-track.js diff --git a/services/discord-gateway/native/libdatachannel-min/binding.cpp b/services/discord-gateway/native/libdatachannel-min/binding.cpp index a7b9dbc..7e70d75 100644 --- a/services/discord-gateway/native/libdatachannel-min/binding.cpp +++ b/services/discord-gateway/native/libdatachannel-min/binding.cpp @@ -123,6 +123,62 @@ class DataChannelWrap : public Napi::ObjectWrap { } }; +class TrackWrap : public Napi::ObjectWrap { + public: + static Function Init(Napi::Env env) { + Function func = DefineClass(env, "Track", { + InstanceMethod("send", &TrackWrap::Send), + InstanceMethod("isOpen", &TrackWrap::IsOpen), + InstanceMethod("close", &TrackWrap::Close), + InstanceMethod("onStateChange", &TrackWrap::OnStateChange), + }); + trackConstructor = Napi::Persistent(func); + return func; + } + + static Object NewInstance(Napi::Env env) { + return trackConstructor.New({}); + } + + TrackWrap(const Napi::CallbackInfo& info) + : Napi::ObjectWrap(info) {} + + void Init(std::shared_ptr track, Napi::Env env) { + track_ = track; + (void)env; + } + + private: + static FunctionReference trackConstructor; + std::shared_ptr track_; + + void Send(const Napi::CallbackInfo& info) { + Buffer buf = info[0].As>(); + if (!track_) return; + rtc::binary data(buf.Length()); + for (size_t i = 0; i < buf.Length(); i++) data[i] = (std::byte)buf[i]; + try { + track_->send(data); + } catch (const std::exception& e) { + fprintf(stderr, "[binding] track.send THREW: %s\n", e.what()); + } + } + + Napi::Value IsOpen(const Napi::CallbackInfo& info) { + bool open = track_ && track_->isOpen(); + return Boolean::New(info.Env(), open); + } + + void Close(const Napi::CallbackInfo& info) { + if (track_) track_->close(); + } + + void OnStateChange(const Napi::CallbackInfo& info) { + // libdatachannel Track has no state-change callback; kept for API parity. + (void)info; + } +}; + class PeerConnectionWrap : public Napi::ObjectWrap { public: static Function Init(Napi::Env env) { @@ -136,6 +192,7 @@ class PeerConnectionWrap : public Napi::ObjectWrap { InstanceMethod("onStateChange", &PeerConnectionWrap::OnStateChange), InstanceMethod("createDataChannel", &PeerConnectionWrap::CreateDataChannel), InstanceMethod("onDataChannel", &PeerConnectionWrap::OnDataChannel), + InstanceMethod("addTrack", &PeerConnectionWrap::AddTrack), }); return func; } @@ -323,6 +380,44 @@ class PeerConnectionWrap : public Napi::ObjectWrap { return obj; } + Napi::Value AddTrack(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + std::string mid = info[0].As().Utf8Value(); + std::string kind = info[1].As().Utf8Value(); + if (!pc_) throw Error::New(env, "peer closed"); + fprintf(stderr, "[binding] addTrack(%s, %s) start\n", mid.c_str(), kind.c_str()); + try { + std::shared_ptr track; + if (kind == "audio") { + // Opus payload type 120 (matches @dank074 CodecPayloadType.opus) + auto desc = rtc::Description::Audio(mid); + desc.addOpusCodec(120); + track = pc_->addTrack(desc); + } else { + // All video codecs with their payload types, matching WebRtcWrapper: + // H264 101/102, H265 103/104, VP8 105/106, VP9 107/108, AV1 109/110 + auto desc = rtc::Description::Video(mid); + desc.addH264Codec(101); + desc.addRtxCodec(102, 101, 90000); + desc.addH265Codec(103); + desc.addRtxCodec(104, 103, 90000); + desc.addVP8Codec(105); + desc.addRtxCodec(106, 105, 90000); + desc.addVP9Codec(107); + desc.addRtxCodec(108, 107, 90000); + desc.addAV1Codec(109); + desc.addRtxCodec(110, 109, 90000); + track = pc_->addTrack(desc); + } + Object obj = TrackWrap::NewInstance(env); + TrackWrap::Unwrap(obj)->Init(track, env); + return obj; + } catch (const std::exception& e) { + fprintf(stderr, "[binding] addTrack THREW: %s\n", e.what()); + throw Error::New(env, e.what()); + } + } + void OnDataChannel(const Napi::CallbackInfo& info) { Function cb = info[0].As(); dcCb_ = std::make_shared( @@ -344,12 +439,14 @@ class PeerConnectionWrap : public Napi::ObjectWrap { Object InitAll(Napi::Env env, Object exports) { exports.Set("PeerConnection", PeerConnectionWrap::Init(env)); exports.Set("DataChannel", DataChannelWrap::Init(env)); + exports.Set("Track", TrackWrap::Init(env)); return exports; } NODE_API_MODULE(libdatachannel_min, InitAll) -// Definition for the static constructor reference. +// Definition for the static constructor references. FunctionReference DataChannelWrap::dcConstructor; +FunctionReference TrackWrap::trackConstructor; } // namespace diff --git a/services/discord-gateway/native/libdatachannel-min/test-track.js b/services/discord-gateway/native/libdatachannel-min/test-track.js new file mode 100644 index 0000000..9bc02c2 --- /dev/null +++ b/services/discord-gateway/native/libdatachannel-min/test-track.js @@ -0,0 +1,33 @@ +// Verify addTrack produces SDP with audio+video media sections. +"use strict"; +const { PeerConnection } = require("./build/Release/datachannel_min.node"); + +const pc = new PeerConnection({ iceServers: [] }); +const audioTrack = pc.addTrack("0", "audio"); +const videoTrack = pc.addTrack("1", "video"); + +pc.onStateChange((s) => console.log("[test-track] state:", s)); + +pc.createOffer().then((sdp) => { + const hasAudio = /^m=audio\s/m.test(sdp); + const hasVideo = /^m=video\s/m.test(sdp); + const audioPts = sdp.match(/a=rtpmap:(\d+) opus/g) || []; + const videoPts = sdp.match(/a=rtpmap:(\d+) H264/g) || []; + console.log("[test-track] SDP bytes:", sdp.length); + console.log("[test-track] m=audio:", hasAudio, "| m=video:", hasVideo); + console.log("[test-track] opus pt:", audioPts, "| H264 pt:", videoPts); + console.log("[test-track] audio track send ok:", typeof audioTrack.send === "function"); + console.log("[test-track] video track send ok:", typeof videoTrack.send === "function"); + const ok = hasAudio && hasVideo && audioPts.length > 0 && videoPts.length > 0; + console.log(ok ? "TRACK TEST PASSED" : "TRACK TEST FAILED"); + pc.close(); + process.exit(ok ? 0 : 1); +}).catch((e) => { + console.error("[test-track] FAILED:", e.message); + process.exit(1); +}); + +setTimeout(() => { + console.error("[test-track] TIMEOUT"); + process.exit(1); +}, 20000);