spike: add addTrack + TrackWrap to libdatachannel-min binding
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).
This commit is contained in:
@@ -123,6 +123,62 @@ class DataChannelWrap : public Napi::ObjectWrap<DataChannelWrap> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class TrackWrap : public Napi::ObjectWrap<TrackWrap> {
|
||||||
|
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<TrackWrap>(info) {}
|
||||||
|
|
||||||
|
void Init(std::shared_ptr<rtc::Track> track, Napi::Env env) {
|
||||||
|
track_ = track;
|
||||||
|
(void)env;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static FunctionReference trackConstructor;
|
||||||
|
std::shared_ptr<rtc::Track> track_;
|
||||||
|
|
||||||
|
void Send(const Napi::CallbackInfo& info) {
|
||||||
|
Buffer<uint8_t> buf = info[0].As<Buffer<uint8_t>>();
|
||||||
|
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<PeerConnectionWrap> {
|
class PeerConnectionWrap : public Napi::ObjectWrap<PeerConnectionWrap> {
|
||||||
public:
|
public:
|
||||||
static Function Init(Napi::Env env) {
|
static Function Init(Napi::Env env) {
|
||||||
@@ -136,6 +192,7 @@ class PeerConnectionWrap : public Napi::ObjectWrap<PeerConnectionWrap> {
|
|||||||
InstanceMethod("onStateChange", &PeerConnectionWrap::OnStateChange),
|
InstanceMethod("onStateChange", &PeerConnectionWrap::OnStateChange),
|
||||||
InstanceMethod("createDataChannel", &PeerConnectionWrap::CreateDataChannel),
|
InstanceMethod("createDataChannel", &PeerConnectionWrap::CreateDataChannel),
|
||||||
InstanceMethod("onDataChannel", &PeerConnectionWrap::OnDataChannel),
|
InstanceMethod("onDataChannel", &PeerConnectionWrap::OnDataChannel),
|
||||||
|
InstanceMethod("addTrack", &PeerConnectionWrap::AddTrack),
|
||||||
});
|
});
|
||||||
return func;
|
return func;
|
||||||
}
|
}
|
||||||
@@ -323,6 +380,44 @@ class PeerConnectionWrap : public Napi::ObjectWrap<PeerConnectionWrap> {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Napi::Value AddTrack(const Napi::CallbackInfo& info) {
|
||||||
|
Napi::Env env = info.Env();
|
||||||
|
std::string mid = info[0].As<String>().Utf8Value();
|
||||||
|
std::string kind = info[1].As<String>().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<rtc::Track> 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) {
|
void OnDataChannel(const Napi::CallbackInfo& info) {
|
||||||
Function cb = info[0].As<Function>();
|
Function cb = info[0].As<Function>();
|
||||||
dcCb_ = std::make_shared<ThreadSafeFunction>(
|
dcCb_ = std::make_shared<ThreadSafeFunction>(
|
||||||
@@ -344,12 +439,14 @@ class PeerConnectionWrap : public Napi::ObjectWrap<PeerConnectionWrap> {
|
|||||||
Object InitAll(Napi::Env env, Object exports) {
|
Object InitAll(Napi::Env env, Object exports) {
|
||||||
exports.Set("PeerConnection", PeerConnectionWrap::Init(env));
|
exports.Set("PeerConnection", PeerConnectionWrap::Init(env));
|
||||||
exports.Set("DataChannel", DataChannelWrap::Init(env));
|
exports.Set("DataChannel", DataChannelWrap::Init(env));
|
||||||
|
exports.Set("Track", TrackWrap::Init(env));
|
||||||
return exports;
|
return exports;
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_API_MODULE(libdatachannel_min, InitAll)
|
NODE_API_MODULE(libdatachannel_min, InitAll)
|
||||||
|
|
||||||
// Definition for the static constructor reference.
|
// Definition for the static constructor references.
|
||||||
FunctionReference DataChannelWrap::dcConstructor;
|
FunctionReference DataChannelWrap::dcConstructor;
|
||||||
|
FunctionReference TrackWrap::trackConstructor;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -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);
|
||||||
Reference in New Issue
Block a user