codename-sendaround/sendaround_client.go

309 lines
8.4 KiB
Go
Raw Normal View History

2019-07-11 12:00:45 +00:00
package sendaround
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"github.com/icedream/sendaround/internal"
"github.com/pion/webrtc/v2"
)
const sendaroundDataChannelLabel = "sendaround"
var defaultServerUrl = &url.URL{
Scheme: "http",
Host: "localhost:8080",
Path: "/",
}
// SendaroundClient is the interface describing all client functionality to send
// or receive file transmissions via the Sendaround service.
type SendaroundClient interface {
// Offer sets up a WebRTC connection to upload files to another peer.
// A token is generated by the handshake server along with a URL that can be
// used in the web browser to transfer files without any dedicated software.
// Both values will be available in the returned Uploader instance.
Offer() (conn *Uploader, err error)
// Receive sets up a WebRTC connection to download files from a peer.
// The token is communicated to the handshake server so that associated
// connection data can be exchanged. Afterwards, a peer-to-peer connection
// is started and data transmission can occur by requesting files through
// the returned Downloader instance.
Receive(token string) (conn *Downloader, err error)
}
type sendaroundClient struct {
serverURL *url.URL
httpClient *http.Client
webrtcConfiguration webrtc.Configuration
webrtc *webrtc.API
}
// SendaroundClientConfiguration is a data struct containing configuration
// parameters for the creation of a SendaroundClient instance.
type SendaroundClientConfiguration struct {
// ServerUrl is the URL of the Sendaround handshake server. Defaults to the main service URL.
ServerURL *url.URL
// WebrtcConfiguration defines specific configuration for WebRTC communication. Defaults to a configuration which uses Google STUN/ICE servers.
WebRTCConfiguration *webrtc.Configuration
// HttpClient can be set to a specific HTTP client instance to use for handshake server communication. Defaults to the Golang default HTTP client.
HTTPClient *http.Client
}
// NewSendaroundClient creates a new instance of the SendaroundClient implementation to send or receive file transmissions via the Sendaround service.
func NewSendaroundClient(cfg *SendaroundClientConfiguration) SendaroundClient {
serverURL := cfg.ServerURL
if serverURL == nil {
serverURL = defaultServerUrl
}
httpClient := cfg.HTTPClient
if httpClient == nil {
httpClient = http.DefaultClient
}
webrtcConfiguration := cfg.WebRTCConfiguration
if webrtcConfiguration == nil {
webrtcConfiguration = &webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
}
return &sendaroundClient{
serverURL: serverURL,
webrtc: webrtc.NewAPI(),
httpClient: httpClient,
webrtcConfiguration: *webrtcConfiguration,
}
}
func (client *sendaroundClient) Offer() (conn *Uploader, err error) {
// Create a new RTCPeerConnection
peerConnection, err := client.webrtc.NewPeerConnection(client.webrtcConfiguration)
/*
peerConnection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
log.Println("ICE connection state changed:", state,
peerConnection.ConnectionState())
})
peerConnection.OnICEGatheringStateChange(func(state webrtc.ICEGathererState) {
log.Println("ICE gathering state changed:", state)
})
peerConnection.OnSignalingStateChange(func(state webrtc.SignalingState) {
log.Println("Signaling state changed:", state)
})
*/
// Create a datachannel with label 'data'
ordered := true
priority := webrtc.PriorityTypeHigh
protocol := "sendaround"
dataChannel, err := peerConnection.CreateDataChannel(sendaroundDataChannelLabel, &webrtc.DataChannelInit{
Ordered: &ordered,
Protocol: &protocol,
Priority: &priority,
})
// Create an offer to send to the browser
offer, err := peerConnection.CreateOffer(nil)
if err != nil {
return
}
// Sets the LocalDescription, and starts our UDP listeners
err = peerConnection.SetLocalDescription(offer)
if err != nil {
panic(err)
}
// Send SDP to server and retrieve new tokens
req, err := http.NewRequest("POST",
client.serverURL.ResolveReference(&url.URL{Path: "offer"}).String(),
strings.NewReader(offer.SDP))
if err != nil {
log.Fatal(err)
}
req.ContentLength = int64(len([]byte(offer.SDP)))
req.Header.Set("Accept", "application/json")
resp, err := client.httpClient.Do(req)
if err != nil {
log.Fatal(err)
return
}
if resp.StatusCode != 200 {
log.Fatalf("Server says: %s", resp.Status)
return
}
defer resp.Body.Close()
result := new(internal.CreateOfferResponse)
if err = json.NewDecoder(resp.Body).Decode(result); err != nil {
return
}
u, err := url.Parse(result.Url)
if err != nil {
return
}
server := &Uploader{
dataChannel: internal.NewDataChannel(dataChannel),
url: u,
token: result.Token,
}
server.init()
go func() {
var err error
defer func() {
if err != nil {
server.changeState(ConnectionState{Type: Failed, Error: err})
}
}()
// Wait for answer SDP from handshake server
req, err := http.NewRequest("POST",
client.serverURL.ResolveReference(&url.URL{
Path: fmt.Sprintf("offer/%s", url.PathEscape(result.AnswerReceivalToken)),
}).String(),
strings.NewReader(offer.SDP))
if err != nil {
log.Fatal(err)
}
req.ContentLength = int64(len([]byte(offer.SDP)))
req.Header.Set("Accept", "application/sdp")
resp, err := client.httpClient.Do(req)
if err != nil {
log.Fatal(err)
return
}
if resp.StatusCode != 200 {
log.Fatalf("Server says: %s", resp.Status)
return
}
defer resp.Body.Close()
answerSdp, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
// Set answer SDP as remote description
if err = peerConnection.SetRemoteDescription(webrtc.SessionDescription{
Type: webrtc.SDPTypeAnswer,
SDP: string(answerSdp),
}); err != nil {
return
}
}()
conn = server
return
}
func (client *sendaroundClient) Receive(token string) (conn *Downloader, err error) {
dataChannelChan := make(chan *webrtc.DataChannel, 1)
// Create a new RTCPeerConnection
peerConnection, err := client.webrtc.NewPeerConnection(client.webrtcConfiguration)
/*
peerConnection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
log.Println("ICE connection state changed:", state)
})
peerConnection.OnSignalingStateChange(func(state webrtc.SignalingState) {
log.Println("Signaling state changed:", state)
})
peerConnection.OnICEGatheringStateChange(func(state webrtc.ICEGathererState) {
log.Println("ICE gathering state changed:", state)
})
*/
peerConnection.OnDataChannel(func(dataChannel *webrtc.DataChannel) {
if dataChannel.Label() != sendaroundDataChannelLabel {
// We expected a sendaround data channel but got something else => protocol violation
peerConnection.Close()
}
dataChannelChan <- dataChannel
})
// Fetch offer SDP
u := client.serverURL.ResolveReference(&url.URL{
Path: fmt.Sprintf("offer/%s", url.PathEscape(token)),
}).String()
req, err := http.NewRequest("GET", u, nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Accept", "application/sdp")
resp, err := client.httpClient.Do(req)
if err != nil {
log.Fatal(err)
return
}
if resp.StatusCode != 200 {
log.Fatalf("Server says: %s", resp.Status)
return
}
offerSdp, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
if err = peerConnection.SetRemoteDescription(
webrtc.SessionDescription{
SDP: string(offerSdp),
Type: webrtc.SDPTypeOffer,
}); err != nil {
return
}
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
return
}
// Sets the LocalDescription, and starts our UDP listeners
err = peerConnection.SetLocalDescription(answer)
if err != nil {
panic(err)
}
// Send SDP to server
req, err = http.NewRequest("POST",
client.serverURL.ResolveReference(&url.URL{
Path: fmt.Sprintf("answer/%s", url.PathEscape(token)),
}).String(),
strings.NewReader(answer.SDP))
if err != nil {
log.Fatal(err)
}
req.ContentLength = int64(len([]byte(answer.SDP)))
req.Header.Set("Accept", "application/json")
resp, err = client.httpClient.Do(req)
if err != nil {
log.Fatal(err)
return
}
if resp.StatusCode != 200 {
log.Fatalf("Server says: %s", resp.Status)
return
}
// TODO - Implement some way to cancel/timeout this
dataChannel := <-dataChannelChan
retval := &Downloader{
dataChannel: internal.NewDataChannel(dataChannel),
}
retval.init()
conn = retval
return
}