package main import "core:encoding/json" import "core:fmt" import "core:math" import "core:strings" PROTOCOL_VERSION :: 1 MAX_FRAME_BYTES :: 4096 Event_Kind :: enum { Pause, Seek, } Playback_Event :: struct { kind: Event_Kind, paused: bool, position: f64, client_id: u64, } Protocol_Error :: enum { None, Invalid_JSON, Expected_Object, Missing_Version, Unsupported_Version, Missing_Type, Unknown_Type, Missing_Paused, Invalid_Paused, Missing_Position, Invalid_Position, Missing_Client_ID, Invalid_Client_ID, } protocol_error_string :: proc(err: Protocol_Error) -> string { switch err { case .None: return "no error" case .Invalid_JSON: return "invalid JSON" case .Expected_Object: return "expected a JSON object" case .Missing_Version: return "missing integer version" case .Unsupported_Version: return "unsupported protocol version" case .Missing_Type: return "missing string type" case .Unknown_Type: return "unknown event type" case .Missing_Paused: return "missing paused field" case .Invalid_Paused: return "paused must be a boolean" case .Missing_Position: return "missing position field" case .Invalid_Position: return "position must be a finite non-negative number" case .Missing_Client_ID: return "missing client_id field" case .Invalid_Client_ID: return "client_id must be a positive integer" } return "unknown protocol error" } parse_protocol_event :: proc(data: []byte) -> (event: Playback_Event, err: Protocol_Error) { root: json.Value if json.unmarshal(data, &root, spec = .JSON) != nil { return {}, .Invalid_JSON } defer json.destroy_value(root) object, ok := root.(json.Object) if !ok { return {}, .Expected_Object } version_value, found := object["version"] if !found { return {}, .Missing_Version } version, version_ok := version_value.(json.Integer) if !version_ok { return {}, .Missing_Version } if version != PROTOCOL_VERSION { return {}, .Unsupported_Version } type_value, type_found := object["type"] if !type_found { return {}, .Missing_Type } type_name, type_ok := type_value.(json.String) if !type_ok { return {}, .Missing_Type } client_id: u64 if client_id_value, client_id_found := object["client_id"]; client_id_found { parsed_id, client_id_ok := client_id_value.(json.Integer) if !client_id_ok || parsed_id <= 0 { return {}, .Invalid_Client_ID } client_id = u64(parsed_id) } switch string(type_name) { case "pause": paused_value, paused_found := object["paused"] if !paused_found { return {}, .Missing_Paused } paused, paused_ok := paused_value.(json.Boolean) if !paused_ok { return {}, .Invalid_Paused } return Playback_Event{kind = .Pause, paused = bool(paused), client_id = client_id}, .None case "seek": position_value, position_found := object["position"] if !position_found { return {}, .Missing_Position } position: f64 #partial switch value in position_value { case json.Integer: position = f64(value) case json.Float: position = f64(value) case: return {}, .Invalid_Position } if position < 0 || math.is_nan(position) || math.is_inf(position) { return {}, .Invalid_Position } return Playback_Event{kind = .Seek, position = position, client_id = client_id}, .None } return {}, .Unknown_Type } encode_protocol_event :: proc(event: Playback_Event, allocator := context.allocator) -> (line: string) { switch event.kind { case .Pause: if event.client_id > 0 { return fmt.aprintf( "{{\"version\":%d,\"type\":\"pause\",\"paused\":%s,\"client_id\":%d}}\n", PROTOCOL_VERSION, "true" if event.paused else "false", event.client_id, allocator = allocator, ) } return fmt.aprintf( "{{\"version\":%d,\"type\":\"pause\",\"paused\":%s}}\n", PROTOCOL_VERSION, "true" if event.paused else "false", allocator = allocator, ) case .Seek: if event.client_id > 0 { return fmt.aprintf( "{{\"version\":%d,\"type\":\"seek\",\"position\":%.6f,\"client_id\":%d}}\n", PROTOCOL_VERSION, event.position, event.client_id, allocator = allocator, ) } return fmt.aprintf( "{{\"version\":%d,\"type\":\"seek\",\"position\":%.6f}}\n", PROTOCOL_VERSION, event.position, allocator = allocator, ) } return strings.clone("", allocator) } encode_welcome :: proc(client_id: u64, allocator := context.allocator) -> string { return fmt.aprintf( "{{\"version\":%d,\"type\":\"welcome\",\"client_id\":%d}}\n", PROTOCOL_VERSION, client_id, allocator = allocator, ) } parse_welcome :: proc(data: []byte) -> (client_id: u64, matched: bool, err: Protocol_Error) { root: json.Value if json.unmarshal(data, &root, spec = .JSON) != nil { return 0, false, .Invalid_JSON } defer json.destroy_value(root) object, object_ok := root.(json.Object) if !object_ok { return 0, false, .Expected_Object } type_value, type_found := object["type"] if !type_found { return 0, false, .Missing_Type } type_name, type_ok := type_value.(json.String) if !type_ok { return 0, false, .Missing_Type } if string(type_name) != "welcome" { return 0, false, .None } matched = true version_value, version_found := object["version"] if !version_found { return 0, true, .Missing_Version } version, version_ok := version_value.(json.Integer) if !version_ok { return 0, true, .Missing_Version } if version != PROTOCOL_VERSION { return 0, true, .Unsupported_Version } client_id_value, client_id_found := object["client_id"] if !client_id_found { return 0, true, .Missing_Client_ID } parsed_id, client_id_ok := client_id_value.(json.Integer) if !client_id_ok || parsed_id <= 0 { return 0, true, .Invalid_Client_ID } return u64(parsed_id), true, .None } format_playback_log :: proc( scope: string, client_id: u64, event: Playback_Event, is_local := false, allocator := context.allocator, ) -> string { if is_local { switch event.kind { case .Pause: return fmt.aprintf( "%s: client %d (you) %s", scope, client_id, "paused" if event.paused else "played", allocator = allocator, ) case .Seek: return fmt.aprintf( "%s: client %d (you) sought to %.3f seconds", scope, client_id, event.position, allocator = allocator, ) } } switch event.kind { case .Pause: return fmt.aprintf( "%s: client %d %s", scope, client_id, "paused" if event.paused else "played", allocator = allocator, ) case .Seek: return fmt.aprintf( "%s: client %d sought to %.3f seconds", scope, client_id, event.position, allocator = allocator, ) } return strings.clone("", allocator) } log_playback_event :: proc(scope: string, client_id: u64, event: Playback_Event, is_local := false) { line := format_playback_log(scope, client_id, event, is_local) defer delete(line) fmt.println(line) }