///|
pub(all) enum Phase {
Connected
Described
Announced
Ready
Playing
Recording
Paused
Ended
} derive(Eq, @debug.Debug)
///|
pub extend Phase with @debug.Debug::{to_repr}
///|
pub extend Phase with Eq::{equal, not_equal}
///|
/// Single outstanding transaction; media can arrive while a response is pending.
pub struct Session {
priv endpoint : Endpoint
priv mut serial : Int32
priv mut pending : Request?
priv mut phase : Phase
priv mut info : SessionInfo?
priv auth : Authenticator?
priv mut attempts : Int32
priv mut deadline : Int64
priv mut last_activity : Int64
priv mut publishing : Bool
priv setup_uris : Array[String]
}
///|
pub fn Session::new(
uri : String,
auth? : Authenticator,
) -> Session raise RtspError {
{
endpoint: Endpoint::parse(uri),
serial: 0,
pending: None,
phase: Connected,
info: None,
auth,
attempts: 0,
deadline: 0L,
last_activity: 0L,
setup_uris: [],
publishing: false,
}
}
///|
pub fn Session::phase(self : Session) -> Phase {
self.phase
}
///|
pub fn Session::info(self : Session) -> SessionInfo? {
self.info
}
///|
pub fn Session::setup_count(self : Session) -> Int32 {
self.setup_uris.length()
}
///|
pub fn Session::request(
self : Session,
verb : String,
uri? : String,
body~ : Bytes = b"",
headers? : Headers,
now~ : Int64 = 0L,
timeout_ms~ : Int32 = 10000,
) -> Request raise RtspError {
require(
self.pending is None && self.phase != Ended,
"outstanding transaction or ended session",
)
require(
timeout_ms > 0 &&
timeout_ms <= 600000 &&
now >= 0L &&
now <= 9223372036854175807L,
"deadline",
)
match verb {
"DESCRIBE" =>
require(
self.phase == Connected || self.phase == Described,
"DESCRIBE phase",
)
"ANNOUNCE" => require(self.phase == Connected, "ANNOUNCE phase")
"SETUP" =>
require(
self.phase == Described ||
self.phase == Announced ||
self.phase == Ready,
"SETUP phase",
)
"PLAY" =>
require(
(self.phase == Ready || self.phase == Paused) &&
self.setup_uris.length() > 0 &&
!self.publishing,
"PLAY phase",
)
"RECORD" =>
require(
self.phase == Ready && self.setup_uris.length() > 0 && self.publishing,
"RECORD phase",
)
"PAUSE" =>
require(self.phase == Playing || self.phase == Recording, "PAUSE phase")
"TEARDOWN" | "OPTIONS" | "GET_PARAMETER" | "SET_PARAMETER" => ()
_ => raise Invalid("unsupported client method")
}
require(self.serial < 2147483647, "CSeq exhausted")
let target = uri.unwrap_or(self.endpoint.url())
self.serial += 1
let req = Request::new(verb, target, self.serial, body~)
if headers is Some(extra) {
for (k, v) in extra.items() {
require(
lower(k) != "cseq" &&
lower(k) != "session" &&
lower(k) != "authorization" &&
lower(k) != "content-length",
"reserved request header",
)
req.headers.add(k, v)
}
}
if self.info is Some(info) {
req.headers.set("Session", info.id)
}
if self.auth is Some(a) {
if a.challenge != "" {
req.headers.set("Authorization", a.authorization(verb, target))
}
}
self.pending = Some(req)
self.deadline = now + timeout_ms.to_int64()
self.attempts = 0
req
}
///|
/// Returns a retry request only for a validated authentication challenge.
pub fn Session::accept(
self : Session,
response : Response,
now~ : Int64 = 0L,
) -> Request? raise RtspError {
let request = match self.pending {
Some(r) => r
None => raise State("unsolicited reply")
}
require(response.cseq() == request.cseq(), "CSeq mismatch")
require(now >= 0L, "negative time")
if response.status < 200 {
return None
}
if response.status == 401 && self.attempts < 2 {
if self.auth is Some(auth) {
let challenges = response.headers.all("WWW-Authenticate")
require(challenges.length() > 0, "missing challenge")
let mut chosen = challenges[0]
for c in challenges {
if c.has_prefix("Digest ") {
chosen = c
break
}
}
auth.challenge(chosen)
require(self.serial < 2147483647, "CSeq exhausted")
self.serial += 1
let retry = Request::new(
request.verb,
request.uri,
self.serial,
body=request.body,
)
for (k, v) in request.headers.items() {
if lower(k) != "cseq" && lower(k) != "authorization" {
retry.headers.set(k, v)
}
}
retry.headers.set(
"Authorization",
auth.authorization(request.verb, request.uri),
)
self.attempts += 1
self.pending = Some(retry)
return Some(retry)
}
}
self.pending = None
self.last_activity = now
if response.status < 200 || response.status >= 300 {
raise Remote(response.status)
}
if response.headers.unique("Session") is Some(value) {
let info = parse_session(value)
if self.info is Some(old) {
require(old.id == info.id, "session identity changed")
}
self.info = Some(info)
}
match request.verb {
"DESCRIBE" => self.phase = Described
"ANNOUNCE" => {
self.phase = Announced
self.publishing = true
}
"SETUP" => {
require(self.info is Some(_), "SETUP missing Session")
if !self.setup_uris.contains(request.uri) {
require(self.setup_uris.length() < 16, "setup track limit")
self.setup_uris.push(request.uri)
}
self.phase = Ready
}
"PLAY" => self.phase = Playing
"RECORD" => self.phase = Recording
"PAUSE" => self.phase = Paused
"TEARDOWN" => {
self.phase = Ended
self.info = None
}
_ => ()
}
None
}
///|
pub fn Session::check_deadline(
self : Session,
now : Int64,
) -> Unit raise RtspError {
if self.pending is Some(_) && now >= self.deadline {
self.pending = None
self.phase = Ended
raise State("transaction timed out; reconnect required")
}
}
///|
pub fn Session::keepalive_due(self : Session, now : Int64) -> Bool {
match self.info {
Some(info) =>
self.pending is None &&
self.phase != Ended &&
now - self.last_activity >= info.timeout_seconds.to_int64() * 500L
None => false
}
}
///|
pub fn Session::close(self : Session) -> Unit {
self.pending = None
self.phase = Ended
self.info = None
}