模块启动

默认的SIP GateWay也是https协议,端口为8088或者8089
如果需要在自己搭建的测试服务上测试SIP GateWay模块,则也需要修改为wss
具体改动如下: 找到/opt/janus/share/janus/demos/siptest.js

var server = "wss://" + window.location.hostname + ":8989";

janus模块介绍-SIP Gateway-LMLPHP
然后重启janus服务,执行

/opt/janus/bin/janus --debug-level=5 --log-file=$HOME/janus-log

打开测试网页,在demos中找到SIP Gateway,点击start开始运行。
janus模块介绍-SIP Gateway-LMLPHP
janus模块介绍-SIP Gateway-LMLPHP
点击f12可以看到demo中Using WebSockets to contact Janus: wss://49.232.250.45:8989
janus模块介绍-SIP Gateway-LMLPHP

模块基本说明

这是Janus的一个简单SIP插件,允许WebRTC对等方在SIP服务器(例如Asterisk)注册,并通过Janus实例调用SIP用户代理。具体地,当连接到插件时,请求对等方提供其SIP服务器证书,即SIP服务器的地址和其用户名/密码。这导致插件在SIP服务器处注册,并代表网络对等方充当SIP客户端。大多数SIP状态和生存期都被插件屏蔽,只有相关事件(例如INVITE和BYE)和功能(呼叫、挂断)可供网络对等方使用:对等方可以在SIP服务器上呼叫扩展或等待传入的INVITE,并且在呼叫期间可以发送DTMF音调。调用可以执行纯RTP或SDES-SRTP。
该插件背后的概念是允许与同一对等方相关联的不同网页,从而允许同一SIP用户同时连接到插件,但只进行一次SIP REGISTER。这同样适用于呼叫:虽然传入的呼叫将被通知给与对等方相关的所有web UI,但只有一个能够接听和接听,这与SIP分叉的工作方式非常相似,但不需要在同一个地方分叉。然而,这一特定功能尚未实现。

SIP Plugin API

我们可以在SIP插件API中发送的所有请求都是异步的,这意味着所有响应(成功和错误)都将作为同一事务的事件传递。
支持的请求包括注册、注销、调用、接受、拒绝、信息、消息、dtmf_info、订阅、取消订阅、传输、录制、保留、取消保留、更新和挂断。顾名思义,register可以用于在SIP注册器处注册用户名以调用和被调用,而unregister则注销该用户名;call用于通过插件向不同的SIP URI发送INVITE,而accept和dence用于在邀请而不是邀请的情况下接受或拒绝呼叫;接送负责有人接送和无人接送(更多详细信息,请参阅有人接送或无人接送);hold和unhold可以分别用于将呼叫挂起和恢复呼叫;info允许您发送一个通用的SIP info请求,而dtmf_info则专注于将info用于dtmf;message是用于向另一个对等方发送SIP消息的方法;subscribe和unsubscribe用于处理SIP事件,即发送subscribe请求,该请求将导致NOTIFY异步事件;相反,录音用于将对话记录到一个或多个.mjr文件中(取决于您想要录制的方向);更新允许您更新现有会话(例如,进行重新协商或强制ICE重新启动);最后,挂断可以用于在任何时候终止通信,或者挂断(BYE)正在进行的呼叫,或者取消/拒绝(cancel/BYE)尚未开始的呼叫。
无论请求如何,错误响应或事件的格式始终如下:

{
        "sip" : "event",
        "error_code" : <numeric ID, check Macros below>,
        "error" : "<error description as a string>"
}

请注意,上面的错误语法指的是插件API消息传递,而不是响应于SIP请求而获得的SIP错误代码,这些错误代码是使用不同的语法通知的:

{
        "sip" : "event",
        "result" : {
                "event" : "<name of the error event>",
                "code" : <SIP error code>,
                "reason" : "<SIP error reason>",
                "reason_header" : "<Reason header text; optional>",
                "reason_header_protocol" : "<Reason header protocol; optional>",
                "reason_header_cause" : "<Reason header cause code; optional>"
        }
}

对于可用的请求,您可以使用注册请求发送一个SIP REGISTER。更准确地说,注册请求可能会导致SIP register,因为这种方法实际上提供了无需注册即可开始使用SIP帐户的方法。例如,所谓的访客注册就是这样的情况:如果你注册为访客,这意味着你将使用From标头中提供的SIP URI进行呼叫,但实际上你不会发送SIP register;这对于向不需要注册的服务(例如,IVR系统或会议桥)发出的呼叫特别有用,但也意味着除非对等方知道您的私人SIP地址,否则您将无法接收呼叫。注册为帮助者时也不会发送SIP REGISTER:正如我们稍后将解释的,帮助者会话仅用于方便使用同一帐户设置同时SIP呼叫。
也就是说,注册请求的格式必须如下:

{
        "request" : "register",
        "type" : "<if guest or helper, no SIP REGISTER is actually sent; optional>",
        "send_register" : <true|false; if false, no SIP REGISTER is actually sent; optional>,
        "force_udp" : <true|false; if true, forces UDP for the SIP messaging; optional>,
        "force_tcp" : <true|false; if true, forces TCP for the SIP messaging; optional>,
        "sips" : <true|false; if true, configures a SIPS URI too when registering; optional>,
        "rfc2543_cancel" : <true|false; if true, configures sip client to CANCEL pending INVITEs without having received a provisional response first; optional>,
        "username" : "<SIP URI to register; mandatory>",
        "secret" : "<password to use to register; optional>",
        "ha1_secret" : "<prehashed password to use to register; optional>",
        "authuser" : "<username to use to authenticate (overrides the one in the SIP URI); optional>",
        "display_name" : "<display name to use when sending SIP REGISTER; optional>",
        "user_agent" : "<user agent to use when sending SIP REGISTER; optional>",
        "proxy" : "<server to register at; optional, as won't be needed in case the REGISTER is not goint to be sent (e.g., guests)>",
        "outbound_proxy" : "<outbound proxy to use, if any; optional>",
        "headers" : "<object with key/value mappings (header name/value), to specify custom headers to add to the SIP REGISTER; optional>",
        "contact_params" : "<array of key/value objects, to specify custom Contact URI params to add to the SIP REGISTER; optional>",
        "incoming_header_prefixes" : "<array of strings, to specify custom (non-standard) headers to read on incoming SIP events; optional>",
        "refresh" : "<true|false; if true, only uses the SIP REGISTER as an update and not a new registration; optional>",
        "master_id" : "<ID of an already registered account, if this is an helper for multiple calls (more on that later); optional>",
        "register_ttl" : "<integer; number of seconds after which the registration should expire; optional>"
}

注册事件将被发回,因为这是一个异步请求。
如果需要,此请求将向具有正确凭据的指定服务器发起SIP REGISTER。401和407响应将被自动处理,因此错误将不会被通知回呼叫者,除非它们是决定性的(例如,错误的凭证)。注册失败将返回名称为registration_failed的错误。相反,成功注册会在注册事件中得到通知,格式如下:

{
        "sip" : "event",
        "result" : {
                "event" : "registered",
                "username" : <SIP URI username>,
                "register_sent" : <true|false, depending on whether a REGISTER was sent or not>,
                "master_id" : <unique ID of this registered session in the plugin, if a potential master>
        }
}

要注销,只需发送一个不带其他参数的注销请求:

{
        "request" : "unregister"
}

与以前一样,将发回一个注销事件。和以前一样,这也会发送一个SIP REGISTER,以防它最初被发送。在未注册的事件中通知成功注销:

{
        "sip" : "event",
        "result" : {
                "event" : "unregistered",
                "username" : <SIP URI username>,
                "register_sent" : <true|false, depending on whether a REGISTER was sent or not>
        }
}

注册后,您可以拨打电话或等待来电:请注意,如果您选择从不发送REGISTER,您将无法接听来电。
要发送SIP INVITE,您可以使用呼叫请求,其格式如下:

{
        "request" : "call",
        "call_id" : "<user-defined value of Call-ID SIP header used in all SIP requests throughout the call; optional>",
        "uri" : "<SIP URI to call; mandatory>",
        "refer_id" : <in case this is the result of a REFER, the unique identifier that addresses it; optional>,
        "headers" : "<object with key/value mappings (header name/value), to specify custom headers to add to the SIP INVITE; optional>",
        "srtp" : "<whether to mandate (sdes_mandatory) or offer (sdes_optional) SRTP support; optional>",
        "srtp_profile" : "<SRTP profile to negotiate, in case SRTP is offered; optional>",
        "secret" : "<password to use to call, only needed in case authentication is needed and no REGISTER was sent; optional>",
        "ha1_secret" : "<prehashed password to use to call, only needed in case authentication is needed and no REGISTER was sent; optional>",
        "authuser" : "<username to use to authenticate as to call, only needed in case authentication is needed and no REGISTER was sent; optional>",
        "autoaccept_reinvites" : <true|false, whether we should blindly accept re-INVITEs with a 200 OK instead of relaying the SDP to the application; optional, TRUE by default>
}

调用事件将被发回,因为这是一个异步请求。
请注意,此请求必须与JSEP优惠相关联:无法通过SIP插件发送无优惠INVITE。这将生成一个SIP INVITE,并根据指示进行发送。虽然100秒Trying不会通知回用户,但在振铃事件中,180秒将会响铃:

{
        "sip" : "event",
        "call_id" : "<value of SIP Call-ID header for related call>",
        "result" : {
                "event" : "ringing",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

如果呼叫被拒绝或发生任何其他错误,则会发回挂断错误事件。如果呼叫被接受,则接受的事件将与被叫方发起的JSEP应答一起发送回用户:

{
        "sip" : "event",
        "call_id" : "<value of SIP Call-ID header for related call>",
        "result" : {
                "event" : "accepted",
                "username" : "<SIP URI of the callee>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

在这一点上,除了PeerConnection相关的考虑因素外,可以认为该调用已建立。SIP插件会自动发送SIP ACK,因此不需要应用程序手动执行任何操作。
请注意,SIP插件通过183个响应响应支持早期媒体。在接收到183响应的情况下,在进度事件中,它将与被叫方发起的JSEP应答一起发送回用户:

{
        "sip" : "event",
        "call_id" : "<value of SIP Call-ID header for related call>",
        "result" : {
                "event" : "progress",
                "username" : "<SIP URI of the callee>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

如果呼叫者收到进度事件,以下接受的事件将不包含JSEP应答,因为在“会话进度”事件中收到的应答将作为会话的SDP应答。
请注意,您只使用call来开始对话,即第一次INVITE。要通过重新邀请更新会话,例如,重新协商会话以添加/删除流或强制ICE重新启动,您不使用调用,而是使用另一个名为update的请求。这个请求不需要任何参数,因为整个上下文都是从会话的当前状态派生的。不过,作为重新谈判的一部分,它确实需要新的JSEP报价。

{
        "request" : "update"
}

更新事件将被发回,因为这是一个异步请求。
虽然呼叫请求允许您发送SIP INVITE(更新请求允许您更新现有会话),但也有一种方法可以对SIP INVITEs做出反应,即处理传入呼叫。传入呼叫通过传入呼叫事件通知应用程序:

{
        "sip" : "event",
        "call_id" : "<value of SIP Call-ID header for related call>",
        "result" : {
                "event" : "incomingcall",
                "username" : "<SIP URI of the caller>",
                "displayname" : "<display name of the caller, if available; optional>",
                "callee" : "<SIP URI that was called (in case the user is associated with multiple public URIs)>",
                "referred_by" : "<SIP URI header conveying the identity of the transferor, if this is a transfer; optional>",
                "replaces" : "<call-ID of the call that this is supposed to replace, if this is an attended transfer; optional>",
                "srtp" : "<whether the caller mandates (sdes_mandatory) or offers (sdes_optional) SRTP support; optional>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

来电可能会也可能不会附带JSEP报价,这取决于来电者发送的是无报价邀请还是常规邀请。无论哪种方式,您都可以通过接受请求来接受来电:

{
        "request" : "accept",
        "srtp" : "<whether to mandate (sdes_mandatory) or offer (sdes_optional) SRTP support; optional>",
        "headers" : "<object with key/value mappings (header name/value), to specify custom headers to add to the SIP OK; optional>"
        "autoaccept_reinvites" : <true|false, whether we should blindly accept re-INVITEs with a 200 OK instead of relaying the SDP to the browser; optional, TRUE by default>
}

由于这是一个异步请求,将发回一个接受事件。
这将导致200 OK被发送回呼叫者。接受请求必须始终附有JSEP回复(如果incomingcall事件包含要约)或要约(如果是无要约邀请)。在前一种情况下,接受的事件将被发回,只是为了确认呼叫可以被视为已建立;相反,在后一种情况下,接受事件将被发送回,而接受的事件只会在稍后,只要在呼叫者发送回的SIP ACK中有JSEP应答可用,就紧随其后。
请注意,如果您在另一个呼叫中接到来电,您将不会收到incomingcall事件,而是收到missed_call事件,这只是一个通知,因为在SIP插件中,无法在同一句柄上同时进行两个呼叫:

{
        "sip" : "event",
        "call_id" : "<value of SIP Call-ID header for related call>",
        "result" : {
                "event" : "missed_call",
                "caller" : "<SIP URI of the caller>",
                "displayname" : "<display name of the caller, if available; optional>",
                "callee" : "<SIP URI that was called (in case the user is associated with multiple public URIs)>"
        }
}

此外,您只能使用accept来回答第一个INVITE。要接受重新邀请(将通过updatingcall事件通知),您不使用accept,而是使用之前引入的更新。这个请求不需要任何参数,因为整个上下文都是从会话的当前状态派生的。不过,作为重新谈判的一部分,它确实需要新的JSEP答案来提供。和以前一样,由于这是一个异步请求,因此会发回一个更新的事件。
关闭会话取决于调用状态。如果您有不想接听的来电,请使用拒绝请求;在所有其他情况下,请改用挂断请求。这两个请求都不需要额外的参数,因为整个上下文可以从插件中会话的当前状态中提取:

{
        "request" : "decline",
        "code" : <SIP code to be sent, if not set, 486 is used; optional>",
        "headers" : "<object with key/value mappings (header name/value), to specify custom headers to add to the SIP request; optional>"
}
{
        "request" : "hangup",
        "headers" : "<object with key/value mappings (header name/value), to specify custom headers to add to the SIP BYE; optional>"
}

由于这些都是异步请求,您将得到一个响应事件:如果使用了谢绝,则拒绝;如果使用了挂起,则挂起。
如前所述,当呼叫被拒绝或挂断时,会发送一个挂断事件,这基本上是一个SIP错误事件通知,因为它包括代码和原因。例如,常规BYE将被通知200和SIP BYE,尽管也可以提供更详细的描述。
建立会话后,可以使用不同的请求与会话进行交互。
首先,您可以使用挂起请求将呼叫挂起。默认情况下,此请求将向对等方发送一个新的INVITE,其中包含媒体的sendonly方向,但如果您想设置不同的方向(recvonly或inactive),也可以通过传递direction属性来实现:

{
        "request" : "hold",
        "direction" : "<sendonly, recvonly or inactive>"
}

持有者方不涉及WebRTC重新协商,因为这只会触发SIP方的重新INVITE。要从挂起状态移除调用,只需向插件发送一个取消挂起的请求,该请求不需要其他属性:

{
        "request" : "unhold"
}

并且将恢复在保持呼叫之前在SDP中设置的媒体方向。
消息请求允许您向对等方发送SIP message。默认情况下,在活动呼叫期间,它在对话框中发送。但是,如果用户已经注册,它也可能被发送到对话框之外。在这种情况下,uri参数是必需的。

{
        "request" : "message",
        "call_id" : "<user-defined value of Call-ID SIP header used to send the message; optional>",
        "content_type" : "<content type; optional>"
        "content" : "<text to send>",
        "uri" : "<SIP URI of the peer; optional; if set, the message will be sent out of dialog>",
        "headers" : "<object with key/value mappings (header name/value), to specify custom headers to add to the SIP MESSAGE; optional>"
}

messagesend事件将被发回。相反,传入的SIP消息会在消息事件中得到通知:

{
        "sip" : "event",
        "result" : {
                "event" : "message",
                "sender" : "<SIP URI of the message sender>",
                "displayname" : "<display name of the sender, if available; optional>",
                "content_type" : "<content type of the message>",
                "content" : "<content of the message>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

传递后,消息传递事件将与SIP服务器响应一起发送回。用于跟踪邮件的传递状态。

{
        "sip" : "event",
        "call_id" : "<value of SIP Call-ID header for related message>",
        "result" : {
                "event" : "messagedelivery",
                "code" : "<SIP error code>",
                "reason" : "<SIP error reason>",
        }
}

SIP INFO的工作方式几乎相同,只是您使用一个对一个对等体的信息请求:

{
        "request" : "info",
        "type" : "<content type>"
        "content" : "<message to send>",
        "headers" : "<object with key/value mappings (header name/value), to specify custom headers to add to the SIP INFO; optional>"
}

将发回一个infosend事件。相反,传入的SIP info会在信息事件中得到通知:

{
        "sip" : "event",
        "result" : {
                "event" : "info",
                "sender" : "<SIP URI of the message sender>",
                "displayname" : "<display name of the sender, if available; optional>",
                "type" : "<content type of the message>",
                "content" : "<content of the message>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

正如预期的那样,使用SUBSCRIBE和NOTIFY机制也支持SIP事件。要做到这一点,您需要使用订阅请求,该请求的格式如下:

{
        "request" : "subscribe",
        "call_id" : "<user-defined value of Call-ID SIP header used in all SIP requests throughout the subscription; optional>",
        "event" : "<the event to subscribe to, e.g., 'message-summary'; mandatory>",
        "accept" : "<what should be put in the Accept header; optional>",
        "to" : "<who should be the SUBSCRIBE addressed to; optional, will use the user's identity if missing>",
        "subscribe_ttl" : "<integer; number of seconds after which the subscription should expire; optional>",
        "headers" : "<array of key/value objects, to specify custom headers to add to the SIP SUBSCRIBE; optional>"
}

如果subscribe请求被接受,则会发回订阅事件,然后是subscribe_succeed,如果事务失败,则会返回subscribe_failed。相反,传入的SIP NOTIFY事件在通知事件中得到通知:

{
        "sip" : "event",
        "call_id" : "<value of SIP Call-ID header for related subscription>",
        "result" : {
                "event" : "notify",
                "notify" : "<name of the event that the user is subscribed to, e.g., 'message-summary'>",
                "substate" : "<substate of the subscription, e.g., 'active'>",
                "content-type" : "<content-type of the message>"
                "content" : "<content of the message>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

您还可以录制SIP调用,它的工作原理与VideoCall插件的工作原理几乎相同。具体来说,您可以使用录制请求来启动或停止录制,使用以下语法:

{
        "request" : "recording",
        "action" : "<start|stop, depending on whether you want to start or stop recording something>"
        "audio" : <true|false; whether or not our audio should be recorded>,
        "video" : <true|false; whether or not our video should be recorded>,
        "peer_audio" : <true|false; whether or not our peer's audio should be recorded>,
        "peer_video" : <true|false; whether or not our peer's video should be recorded>,
        "send_peer_pli" : <true|false; whether or not send PLI to request keyframe from peer>,
        "filename" : "<base path/filename to use for all the recordings>"
}

正如您所看到的,这意味着对话的双方是分开录制的,音频和视频流也是如此(如果可用)。您可以选择要记录的内容,以防您只对子集感兴趣。文件名部分只是一个前缀,指示将用于最多四个可能需要启用的录制的实际文件名。
如果请求成功,则会发回已记录的更新事件。

Simultaneous SIP calls using the same account

如前几节所述,使用Janus句柄附加到SIP插件意味着代表用户或应用程序创建SIP堆栈:这通常意味着注册帐户,并能够启动或接收调用、处理订阅等。这也意味着,由于在Janus中,每个核心句柄只能与单个PeerConnection相关联,因此每个SIP帐户每次仅限于一个调用:如果用户已经在SIP会话中,并且另一个调用传入,则会自动拒绝,并显示486 Busy。
虽然通常不是什么大问题,但在某些用例中,支持多个并发调用是有意义的,并且可以从一个无缝地切换到另一个。这在使用所谓的助手会话的SIP插件中是可能的。具体来说,助手会话在假设存在正常注册的主会话(即“常规”SIP插件句柄)的情况下工作,并且这些助手会话可以简单地与之关联:任何时候需要另一个并发调用,如果主会话繁忙,则可以使用其中一个助手;可用的助手会话越多,可以建立的同时调用就越多。
其工作方式很简单:
您以通常的方式创建SIP会话,并在那里发送常规注册表;这将是主会话,并在成功注册时返回master_id;
对于要添加的每个助手,您将新的Janus句柄附加到SIP插件,并发送一个类型为“helper”的寄存器,并提供与主会话相同的用户名,以及引用主会话的master_id属性;
此时,新的助手与主机相关联,这意味着它可以用于启动新的调用或接收与主会话完全相同的调用,并使用相同的帐户信息、凭据等。
请注意,一旦主服务器注销,或者它所在的Janus句柄被分离,与它相关的所有助手会话也会自动删除。具体来说,插件将强制分离相关句柄。如果您需要再次注册,并希望在那里也有一些助手,您将不得不再次添加他们。
如果您想在实践中看到这一点,SIP插件演示有一个“隐藏”函数,您可以从JavaScript控制台调用该函数来玩助手:调用addHelper()函数将添加一个新的助手,并显示其他控件。您可以根据需要添加任意数量的辅助对象。

Attended and blind transfers

Janus SIP插件支持有人参与和盲传输,并且这样做主要依赖于多个调用功能:因此,请确保您已经阅读并熟悉关于使用相同帐户进行同步SIP调用的部分。
大多数与传输相关的功能都基于上一节中已经记录的现有消息和事件,但有几个方面需要注意。首先,如果您是传输者,则需要使用名为transfer的新请求,该请求允许您向受让者发送SIPREFER,以便到达不同的目标。传输请求的格式必须如下所示:

{
        "request" : "transfer",
        "uri" : "<SIP URI to send the transferee too>",
        "replace" : "<call-ID of the call this attended transfer is supposed to replace; default is none, which means blind/unattended transfer>"
}

无论是盲传输(无替换调用)还是有人参与传输,都将发回传输事件,因为这是一个异步请求。进一步的更新将以NOTIFY相关事件的形式出现,因为REFER隐式创建订阅。
相反,REFER的接收者将接收一个名为transfer的异步事件,其中包含它需要知道的信息。事实上,SIP插件不会自动执行任何操作:传入的REFER被通知给应用程序,以便它可以决定是否跟踪传输。事件的语法如下:

{
        "sip" : "event",
        "result" : {
                "event" : "transfer",
                "refer_id" : <unique ID, internal to Janus, of this referral>,
                "refer_to" : "<SIP URI to call>",
                "referred_by" : "<SIP URI SIP URI header conveying the identity of the transferor; optional>",
                "replaces" : "<call-ID of the call this transfer is supposed to replace; optional, and only present for attended transfers>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

该列表中最重要的属性是refer_id,因为如果传输被接受,则该值必须包含在调用请求中以调用目标:事实上,这是SIP插件必须将新的传出调用与前一个传输请求相关联的唯一方法,从而能够通过notify事件通知传输者调用的进展情况。请注意,如果受让人决定跟踪转移请求,并且他们已经在通话中(例如,与转发器),则他们必须为此目的使用不同的句柄,例如,通过使用相同帐户部分的同步SIP呼叫中描述的助手。
传输目标将完全像前面讨论的那样接收调用,不同的是,出于信息目的,它可能包含或不包含referred_by属性。就像受让人一样,如果他们已经在调用中,则应由应用程序创建一个助手来设置一个新的Janus句柄以接受传输。
请注意,插件不会将涉及的调用置于等待状态,或自动关闭打算由传输替换的调用。所有这些都是应用程序的责任,因此开发人员应该相应地对事件做出反应。

04-23 11:27