Last active
February 6, 2024 14:38
-
-
Save freehuntx/2f5c52221fc2952f05df20b91a7adffd to your computer and use it in GitHub Desktop.
Godot Webtorrent
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extends RefCounted | |
## Classes/Constants | |
enum State { NEW, CLOSED, CONNECTING, CONNECTED } | |
class Response: | |
var info_hash: String # The info_hash which the repsonse belongs to | |
var peer_id: String # The peer_id of the other peer (who've sent it) | |
var offer_id: String # The offer_id that this offer/answer belongs to | |
var sdp: String # The sdp (webrtc session description) of the other peer | |
## Signals | |
signal connecting | |
signal connected | |
signal disconnected | |
signal closed | |
signal failure(reason: String) | |
signal got_offer(offer: Response) | |
signal got_answer(answer: Response) | |
## Members | |
var _state := State.NEW | |
var _socket: WebSocketPeer # An internal reference to the websocket peer | |
var _peer_id: String # Our peer_id that is used to identify us | |
var _tracker_url: String # The tracker we are connected to | |
var _max_connections: int | |
var peer_id: String: | |
get: return _peer_id | |
var is_connected: | |
get: return _state == State.CONNECTED | |
## Statics | |
static func gen_id() -> String: | |
const charset := "0123456789aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ" | |
var pid: String | |
var n_char = len(charset) | |
for i in 20: | |
pid += charset[randi()% n_char] | |
return pid | |
## Constructor | |
func _init(options:={}): | |
_peer_id = options.peer_id if "peer_id" in options else gen_id() | |
_max_connections = options.max_connections if "max_connections" in options else 40 | |
## Public methods | |
func connect_to_server(url: String) -> Error: | |
if _socket != null: | |
return Error.ERR_ALREADY_IN_USE | |
_socket = WebSocketPeer.new() | |
# When not in web we should use an useragent. Some servers dont accept requests without an user-agent | |
if OS.get_name() != "Web": | |
_socket.handshake_headers = PackedStringArray([ "user-agent: Godot" ]) | |
var err := _socket.connect_to_url(url) | |
if err != OK: | |
_socket = null | |
return err | |
_tracker_url = url | |
_state = State.CONNECTING | |
connecting.emit() | |
return Error.OK | |
func close() -> Error: | |
if _socket == null: | |
return Error.ERR_DOES_NOT_EXIST | |
var old_state = _state | |
_socket.close() | |
_socket = null | |
_state = State.CLOSED | |
if old_state == State.CONNECTED: | |
disconnected.emit() | |
closed.emit() | |
return Error.OK | |
func answer(info_hash: String, offer_peer_id: String, offer_id: String, answer_sdp: String) -> void: | |
if not is_connected: | |
connected.connect(answer.bind(info_hash, offer_peer_id, offer_id, answer_sdp), CONNECT_ONE_SHOT) | |
return | |
_socket.send_text(JSON.stringify({ | |
"action": "announce", | |
"info_hash": info_hash, | |
"peer_id": _peer_id, | |
"to_peer_id": offer_peer_id, | |
"offer_id": offer_id, | |
"answer": { | |
"type": "answer", | |
"sdp": answer_sdp | |
} | |
})) | |
func announce(info_hash: String, offers: Array) -> void: | |
if not is_connected: | |
connected.connect(announce.bind(info_hash, offers), CONNECT_ONE_SHOT) | |
return | |
_socket.send_text(JSON.stringify({ | |
"action": "announce", | |
"info_hash": info_hash, | |
"peer_id": _peer_id, | |
"numwant": offers.size(), | |
"offers": offers | |
})) | |
func poll() -> void: | |
if _socket == null or _state < State.CONNECTING: | |
return | |
_socket.poll() | |
var ready_state = _socket.get_ready_state() | |
if ready_state != WebSocketPeer.STATE_OPEN: | |
if ready_state != WebSocketPeer.STATE_CONNECTING: | |
close() # Not open? Not connecting? Then its closed! | |
return | |
if _state == State.CONNECTING: | |
_state = State.CONNECTED | |
connected.emit() | |
while _socket.get_available_packet_count(): | |
_on_packet(_socket.get_packet()) | |
## Callbacks | |
func _on_packet(buffer: PackedByteArray) -> void: | |
var str := buffer.get_string_from_utf8() | |
var data = JSON.parse_string(str) | |
if data == null or typeof(data) != TYPE_DICTIONARY: | |
push_error("[WebSocketClient] Invalid json: %s" % [str]) | |
return | |
if "failure reason" in data: | |
_on_failure(data["failure reason"]) | |
return | |
if not "action" in data or data.action != "announce": | |
return | |
if not "info_hash" in data or not "peer_id" in data or not "offer_id" in data: | |
return | |
var response := Response.new() | |
response.info_hash = data.info_hash | |
response.peer_id = data.peer_id | |
response.offer_id = data.offer_id | |
if "offer" in data: | |
response.sdp = data.offer.sdp | |
got_offer.emit(response) | |
elif "answer" in data: | |
response.sdp = data.answer.sdp | |
got_answer.emit(response) | |
func _on_failure(reason: String): | |
push_error("Error: ", reason) | |
failure.emit(reason) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extends RefCounted | |
## Classes/Constants | |
enum Type { NONE, OFFER, ANSWER } | |
enum State { NEW, CLOSED, GATHERING, CONNECTING, CONNECTED } | |
## Signals | |
signal gathered | |
signal connecting | |
signal connected | |
signal disconnected | |
signal closed | |
## Members | |
var _type := Type.NONE | |
var _state := State.NEW | |
var _connection := WebRTCPeerConnection.new() | |
var _event_channel: WebRTCDataChannel | |
var _event_listener := {} | |
var _answered := false | |
var _local_sdp: String | |
var _remote_sdp: String | |
var state: State: | |
get: return _state | |
var type: Type: | |
get: return _type | |
var connection: WebRTCPeerConnection: | |
get: return _connection | |
var local_sdp: String: | |
get: return _local_sdp | |
## Constructor | |
func _init(): | |
_connection.session_description_created.connect(self._on_session_description_created) | |
_connection.ice_candidate_created.connect(self._on_ice_candidate_created) | |
## Public methods | |
func initialize() -> Error: | |
var err := _connection.initialize({ "iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}] }) | |
if err == OK: | |
_event_channel = _connection.create_data_channel("events", { "id": 555, "negotiated": true }) | |
return err | |
func create_offer() -> Error: | |
var err := _connection.create_offer() | |
if err == OK: | |
_type = Type.OFFER | |
_state = State.GATHERING | |
return err | |
func create_answer(offer_sdp: String) -> Error: | |
var err := _connection.set_remote_description("offer", offer_sdp) | |
if err == OK: | |
_type = Type.ANSWER | |
_state = State.GATHERING | |
_remote_sdp = offer_sdp | |
return err | |
func answer(answer_sdp) -> Error: | |
if _type != Type.OFFER: | |
return ERR_INVALID_DECLARATION | |
if _answered: | |
return ERR_ALREADY_EXISTS | |
var err := _connection.set_remote_description("answer", answer_sdp) | |
if err == OK: | |
_answered = true | |
_remote_sdp = answer_sdp | |
return err | |
func close() -> void: | |
if _state == State.CLOSED: | |
return | |
_connection.close() | |
if _state == State.CONNECTED: | |
disconnected.emit() | |
_state = State.CLOSED | |
closed.emit() | |
func poll() -> void: | |
if _state < State.GATHERING: | |
return | |
var err := _connection.poll() | |
if err != OK: | |
return | |
if _state == State.GATHERING: | |
var gathering_state := _connection.get_gathering_state() | |
if gathering_state != WebRTCPeerConnection.GATHERING_STATE_COMPLETE: | |
return | |
_state = State.CONNECTING | |
gathered.emit() | |
var connection_state := _connection.get_connection_state() | |
if _state == State.CONNECTING: | |
if connection_state == WebRTCPeerConnection.STATE_CONNECTING: | |
return | |
if connection_state != WebRTCPeerConnection.STATE_CONNECTED: | |
close() | |
return | |
_state = State.CONNECTED | |
connected.emit() | |
if connection_state != WebRTCPeerConnection.STATE_CONNECTED: | |
close() | |
return | |
while _event_channel.get_available_packet_count(): | |
var buffer := _event_channel.get_packet() | |
var args = bytes_to_var(buffer) | |
if typeof(args) != TYPE_ARRAY or args.size() < 1 or typeof(args[0]) != TYPE_STRING: | |
continue | |
if args.size() == 2 and typeof(args[1]) != TYPE_ARRAY: | |
continue | |
_on_event.callv(args) | |
func send_event(event_name: String, event_args:=[]) -> Error: | |
var pack_array = [event_name] | |
if event_args.size() > 0: | |
pack_array.append(event_args) | |
return _event_channel.put_packet(var_to_bytes(pack_array)) | |
func once_event(event_name: String, callback: Callable) -> Callable: | |
return on_event(event_name, callback, true) | |
func on_event(event_name: String, callback: Callable, once:=false) -> Callable: | |
if not event_name in _event_listener: _event_listener[event_name] = [] | |
var listener = [callback, once] | |
_event_listener[event_name].append(listener) | |
return off_event.bind(event_name, callback, once) | |
func off_event(event_name: String, callback: Callable, once:=false) -> void: | |
if not event_name in _event_listener: return | |
_event_listener[event_name] = _event_listener[event_name].filter(func(e): return e[0] != callback and e[1] != once) | |
## Callbacks | |
func _on_session_description_created(type: String, sdp: String) -> void: | |
_local_sdp = sdp | |
_connection.set_local_description(type, sdp) | |
func _on_ice_candidate_created(media: String, index: int, name: String) -> void: | |
_local_sdp += "a=%s\r\n" % [name] | |
func _on_event(event_name: String, event_args:=[]) -> void: | |
if not event_name in _event_listener: return | |
# Remove null instance callbacks | |
_event_listener[event_name] = _event_listener[event_name].filter(func(e): return e[0].get_object() != null) | |
for listener in _event_listener[event_name]: | |
listener[0].callv(event_args) | |
# Remove once listeners | |
_event_listener[event_name] = _event_listener[event_name].filter(func(e): return not e[1]) | |
if _event_listener[event_name].size() == 0: | |
_event_listener.erase(event_name) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment