Skip to content

Instantly share code, notes, and snippets.

@freehuntx
Last active February 6, 2024 14:38
Show Gist options
  • Save freehuntx/2f5c52221fc2952f05df20b91a7adffd to your computer and use it in GitHub Desktop.
Save freehuntx/2f5c52221fc2952f05df20b91a7adffd to your computer and use it in GitHub Desktop.
Godot Webtorrent
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)
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