WebRTC 实现一对一视频通话会议

1、通话流程

一对一的视频通话连接流程和第三章的连接流程一样,学者可以详细阅读第三章的内容和实际操示例。整个通话的流程相对来说还是比较复杂,需要借助信令服务器和STUN服务器。整个系统设计如下

1.正常的用户体系,包括用户登录,注册,查看用户列表,请求通话,通话详情页面,用户挂断,用户退出等基础功能

2.用户A:作为会话的发起方,创建提议offer

3.用户B:会话应答方接到A发来的提议后创建应答answer

4.信令服务器:websocket服务连接用户A和用户B,转发双方的SDP及Candidate信息,以及用户上下线,请求通话,挂断,拒绝通话等消息。

5.STUN服务器:用于接收用户A 、B的ICE请求,从而获取各自的Candidate信息,再通过信令服务器转发至双方,另外STUN服务器也有转发媒体数据的功能

6.服务连接,双方获取本地媒体流及交换媒体流。

2、技术框架

WebRTC 实现一对一视频通话会议

在本次实现该功能的技术路线中可以看到客户端为web PC-A和PC-B 服务器需要两个具体如下

1.PC web端:web端使用的是vue+h5实现了用户注册登录,进入聊天列表,发起视频请求等功能

2.信令服务:本次使用
github.com/gorilla/websocket 包实现了websocket功能,主要用来用户登录,离线,消息通知,和转发Offer、Answer,Candidate等数据。

3.web服务:使用golang的gin包实现了https服务,主要提供了页面渲染,群聊,mysql存贮用户数据等

4.STUN服务器:本次使用turnserver 服务实现STUN及TURN媒体数据中转,可以用现有服务,也可以自行安装

4、信令设计

信令就是两个客户端之间信息交换的数据,如会话,开始通话,结束通话,用户上线,offer,answer等数据的交换,需要提供统一的数据格式,根据不同的消息类型客户端和服务端处理不同的数据业务。

// 接收定义消息结构
type ReceiveMessage struct {
   // 请求的方法
   Method string `json:"method"`
   // 消息类型(1,text,0:系统消息)
   Type int8 `json:"type"`
   // 消息体
   Message string `json:"message"`
   // 消息来源用户Id
   FromId string `json:"fromId"`
   // 当前连接
   Client *websocket.Conn `json:"client"`
   // 数据参数
   Data map[string]interface{} `json:"data"`
}

// 发送消息结构
type SendMessage struct {
	// 请求的方法
	Method string `json:"method"`
	// 消息类型(1,text,0:系统消息))
	Type uint8 `json:"type"`
	// 消息体
	Message string `json:"message"`
	// Code 0 正确,1 错误
	Code int16 `json:"code"`
	// 数据参数
	Data interface{} `json:"data"`
}

领取音视频开发资料:音视频流媒体高级开发
FFmpegWebRTCRTMPRTSPHLSRTP播放器

WebRTC 实现一对一视频通话会议

企鵝君羊994289133领取资料

WebRTC 实现一对一视频通话会议

本次设计的主要信令方法 method 有如下几种

WebRTC 实现一对一视频通话会议

let data={    "Method": "Message/Candidate",    "Type": 0,    "Message": that.user.name + "发给" + userId + "candidate信息",    "fromId": that.user.id,    "toId": userId,    "data": {        "candidate": {            "sdpMlineIndex": event.candidate.sdpMLineIndex,            "sdpMid": event.candidate.sdpMid,            "candidate": event.candidate.candidate        }    }}send(data)

不同的业务发送不同的数据类型,到服务器,服务器根据toId将消息转发给对应的用户id,在用户收到消息后,需要根据不同的返回业务码来处理业务,业务处理主要在websocket的onmessage回调方法中

onmessage: function () {
    let that = this

    console.log(that.users)

    that.ws.onmessage = function (event) {
        let msg = JSON.parse(event.data)
        console.log("收到消息-----")
        console.log(msg)
        if (msg.code == 10000) {
            that.users = msg.data
        }
        if (msg.code == 10005) {
            that.addMessage('from ' + msg.data.username + ": ", msg.data.message);
            return;
        }
        if (msg.code == 10006) {
            // 有用户登录
            console.log(that.users)
            if (that.users == null) {
                that.users = []
                that.users.push(msg.data)
                return;
            }
            let isIn = 0

            for (let i = 0; i < that.users.length; i++) {
                if (that.users[i].id == msg.data.id) {
                    isIn = 1
                }
            }
            if (isIn == 0) {
                that.users.push(msg.data)
            }
        }
        if (msg.code == 10008) {
            let userId = msg.data.fromId
            let newUser = []
            for (let i = 0; i < that.users.length; i++) {
                if (that.users[i].id != userId) {
                    newUser.push(that.users[i])
                }
            }
            that.users = newUser
        }
        if (msg.code == 10009) {
            that.onCandidate(msg)
            return;
        }
        if (msg.code == 10010) {
            that.onOffer(msg)
            return;
        }
        if (msg.code == 10011) {
            that.onAnswer(msg)
            return;
        }


        that.addMessage('系统消息', msg.message);
    }

5、服务后台

main.go 是整个函数的入口函数使用goang 的gin框架实现了http服务,包括路由websocket路由等

package main

import ( 
   "ginweb/controllers/wss"
   "ginweb/dao"
   "ginweb/route"
   "ginweb/runtime"
   "github.com/gin-gonic/gin"
)

func init() {
   go wss.HandleMessages()
}

func main() {

   config.InitConfig()
   runtime.InitLog()
   dao.Install()
   defer dao.Uninstall()

   r := route.RegisterRouters()  // 注册路由

   r.GET("/wss", wss.OnWssMessage) // websockt路由
   r.LoadHTMLGlob("www/**/**/*") // 加载静态文件
   r.StaticFS("/static", http.Dir("./static"))
   r.Run(":" + config.Data.Port)
}

route.go路由文件加载路由

package route

import (
   "ginweb/controllers/blog"
   "ginweb/controllers/elasticSearch"
   "ginweb/controllers/game/gobang"
   "ginweb/controllers/webrtc"
   "github.com/gin-gonic/gin"
)

// @Title 注册路由
// @return  route  *gin.Engine

func RegisterRouters() *gin.Engine {
   r := gin.Default()
   webrtcGroup := r.Group("/webrtc")
   {
      webrtcGroup.GET("/login", webrtc.LoginPage)
      webrtcGroup.POST("/login", webrtc.Login)
      webrtcGroup.POST("/register", webrtc.Register)
      webrtcGroup.GET("/admin", webrtc.ShowHead)
      webrtcGroup.Use(webrtc.LoginAuth(r)) // 验证登录
      // webrtc 基本操作
      webrtcGroup.GET("/in", webrtc.Home)
      webrtcGroup.GET("/wss", webrtc.OnWsMessage)
   }
   return r
}

wss.go文件 实现了websockt用户连接及断开服务的数据绑定等

func OnWsMessage(req *gin.Context) {
   var loginMsg webrtc.LoginMessage
   r := req.Request
   w := req.Writer
   c, err := upgrader.Upgrade(w, r, nil)
   if err != nil {
      zaplogger.Error(err)
      c.Close()
      return
   }

   err = c.ReadJSON(&loginMsg)
   if err != nil {
      zaplogger.Error(err)
      c.Close()
      return
   }

   // 关闭连接需要修改
   defer logout(loginMsg, c)

   if loginMsg.FromId <= 0 {
      return
   }

   // 2.接收到用户连接,执行登录
   code, res := login(loginMsg, c)
   if code > 0 {
      zaplogger.Error("connect error:"+res, code, loginMsg)
      return
   }

   // 系统监听用户消息
   for {
      // 1.处理当前用户获取系统消息
      var userMsg webrtc.ReceiveMessage
      err = c.ReadJSON(&userMsg)
      if err != nil {
         zaplogger.Error("收到消息 json解析err:", err)
         break
      }
      if userMsg.Method == "User/Connect" {
         continue
      }

      // TODO message 去掉 client 和指针
      zaplogger.Info("收到消息:->", userMsg)
      code, res := GetRouter(userMsg)
      zaplogger.Info("处理结果:->", code, res)
   }

}


message.go 文件主要包括转发会话消息等

// @Title SendToAll
// @Description 批量消息发送给所有在线用户
// @Param   message    用户消息
// @return   code  int16  返回码
// @return   message  string   消息

func (m *Message) Candidate(message webrtc.ReceiveMessage) (code int16, res string) {
   var returnData map[string]interface{}
   returnData = make(map[string]interface{})
   returnData["fromId"] = message.FromId
   returnData["data"] = message.Data
   m.SendToIds([]int32{message.ToId}, webrtc.MessageCandidate, message.Message, message.Method, returnData, 1)
   return
}

// @Title SendToAll
// @Description 批量消息发送给所有在线用户
// @Param   message    用户消息
// @return   code  int16  返回码
// @return   message  string   消息

func (m *Message) Offer(message webrtc.ReceiveMessage) (code int16, res string) {
   var returnData map[string]interface{}
   returnData = make(map[string]interface{})
   returnData["fromId"] = message.FromId
   returnData["data"] = message.Data
   m.SendToIds([]int32{message.ToId}, webrtc.MessageCreateOffer, message.Message, message.Method, returnData, 1)
   return
}

func (m *Message) Answer(message webrtc.ReceiveMessage) (code int16, res string) {
   var returnData map[string]interface{}
   returnData = make(map[string]interface{})
   returnData["fromId"] = message.FromId
   returnData["data"] = message.Data
   m.SendToIds([]int32{message.ToId}, webrtc.MessageAnswer, message.Message, message.Method, returnData, 1)
   return
}

6、服务部署

1、本次前端采用layui+h5+vue.js 实现页面的组件展示,因此需要在项目中导入layui.js和vue.js

2、后台web服务采用golang的gin框架实现http服务及页面功能,在服务器需要安装golang 1.16.3版本

wget https://studygolang.com/dl/golang/go1.16.1.linux-amd64.tar.gz 
tar -C /usr/local -xzf go1.16.1.linux-amd64.tar.gz 
vim /etc/profile 
export GOROOT=/usr/local/go #设置为go安装的路径 
export GOPATH=/home/gocode #默认安装包的路径 
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
source /etc/profile  // 生效
go env -w GOPROXY=https://goproxy.cn,direct

3、golang 1.14版本后使用go.mod进行包管理创建项目很简单,进入到项目的根目录下执行

go mod init ginweb
go mod tidy
go run main.go  

4、webrtc需要通过域名+https访问因此服务器需要打开80,443,以及stun服务器的 3478 端口。另外https服务需要提供证书。本次部署我是采用宝塔面板安装简单,http和websocket服务只需要做端口转发即可

三、总结

通过前面几章节的学习后我们学会了,操作本地媒体流,媒体操作渲染,数据通道,数据发送等基本功能,并且实现了信令服务,STUN服务器等单独功能,前期准备工作做好后就可以将所有功能整合起来,实现webrtc一对一视频通话功能。在实际处理过程中需要注意以下问题。

1、在用户发送会话提议offer和Answer的时候逻辑代码在一个页面书比较复杂而且挺绕,需要提前熟悉webrtc的连接流程,指导原理后书写就很简单。

2、系统中的用户体系在聊天界面中可以实现消息会话以及实现发起聊天,挂断等操作。

3、stun服务器目前网上有很多免费的但是好多都不可用,如果自己不想搭建的话 可以使用免费的,但是使用前需要测试可用性
https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/ 该网址可以测试

4、自行搭建的STUN服务器一般都自带turn服务。搭建完成后依然需要自行测测试

5、关于STUN服务器的原理以及webrtc端到端的连接原理会在下一章整理。

本文链接:https://www.dzdvip.com/33200.html 版权声明:本文内容均来源于互联网。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 395045033@qq.com,一经查实,本站将立刻删除。
(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022年5月22日 16:43
下一篇 2022年5月22日 16:48

相关推荐

  • 什么是跨境电商?一文彻底搞懂跨境电商

    是时候系统性地说这件事了,先说观点: 如果说2010~2020年中国品牌出海的黄金十年,那么我认为2020年之后的十年将是未来中国优质产品出海的白银十年。 如何把握好这次机会,至关重要。 跨境贸易的机会本质在于全球市场的不均衡性,不同的市场处于不同的发展阶段,互联网 供应链,放眼全球,能做全球电商的大概率只有中国了。 本文的主要用户是具有优质原创产品项目,但还没正式进军海外市场的人士。内容核心解决以下三大问题: 1、优质原创产品品牌出海的主要路径 2、产品出海的常见雷区以及避免办法 3、跨境电商超级大卖家的常见组织架构 文章很长,可以先收藏后,找个大块时间,耐心看完。 一、优质原创产品品牌出海的四大路径 现阶段我们怎样才能更高效地做好产品出海? 国际贸易的蓬勃发展催生了越来越多的贸易方式,整体来说,主要是下面四种途径: 1、国内批发模式 如果你的产品比较新颖,适合国外某些区域的市场需求,即使你没有直接做海外市场,你肯定也会遇到有外贸贸易公司直接向你采购产品,再由他们通过海外B2B或者B2C的方式,将产品卖到海外去,这类常见于以生产研发为主的工厂型企业或者产品团队,专注产品的研发。 2、外贸B2B模式 这是比较传统也是目前依然占中国出口比例最大的贸易模式,通过批发的模式,直接对接海外的批发商,再由他们通过当地的电商平台或者线下门店销售出去,这类模式弊端就是我们没有直接接触海外C端客户,没办法及时获取前端市场一手信息,产品的高溢价也被海外分销商拿走。 3、跨境B2C模式 大概在2014年之前,这个还是叫做外贸B2C,后来慢慢发展出了跨境电商的叫法,在这个领域被当作优等生天天被行业里面的卖家以及行业外的媒体学习研究的,自然是安克创新(市值700亿人民币左右,前身为海翼电商)旗下的第一个自主产品品 Anker。 Anker早期的成功一方面是产品深度击中海外用户的痛点和解决了刚需,另一方面是抓住了移动互联网发展带来的换机潮以及亚马逊全球开店的早期电商超级红利,至少从我现在来看,在其他垂直品类赛道想再要复制Anker在电源领域的成就,应该是难度比较高了。 4、海外本土化运营模式 跨境电商是非常适合产品初期进入海外市场,直接面向海外C端用户,提高产品溢价的有效方式,但始终是跨境,等到体量发展到一定规模后,会发现最终还是需要落地,国际贸易的生意不能一直隔着屏幕进行,否则永远只是一个…

    2021年6月10日
    10
  • 如何打造出汽车行业短视频爆款视频?

    短视频现在是最火爆的,大量的流行语或者流行歌曲都从短视频流传出来的,作为最火爆的短视频软件,很多汽车行业都在慢慢进攻短视频来成为商家,那么2019年如何打造汽车行业短视频爆款短视频? 1、案牍 在写短视频视频案牍的时分使用精华提炼、设置悬念、亮点预告、实在动情等方式,让用户能够自行的把视频画面和案牍配合起来进行观看。 尽管说汽车行业短视频是以视频为主的社交使用平台,可是用户在看视频的时分也会不自觉的关注案牍内容。所以说一个凶猛的案牍绝对能够成为爆款视频的点睛之笔。 2、开门见山 我们能够经过设置封面并且在开头抛出问题和利益点,来直接的捉住用户的好奇心思。也便是说在视频的开始这届给出相应的结论,剩余的时刻都用来解答疑惑。 拍摄汽车行业短视频视频的时分能够在视频开头就明确地告知用户视频的主题回则是主要内容,在接下来的视频里面就对此进行详细的讲解。只要是我们抛出的主题满足有趣就能够激发用户的爱好。 3、制造悬念 经常刷短视频的朋友们都知道,短视频视频离不开的便是背景音乐。一个适宜的背景音乐也会成为爆款的关键因素之一。从街头巷尾越来越多的短视频“神曲”就能够窥见。 使用背景音乐来制造悬念能够分为两种,第一种方法便是使用不同的音乐类型来对应不同的情绪,第二种便是直接使用某个短视频达人带火的音乐来直接的对应视频。 4、视觉 我们在拍摄汽车行业短视频视频的时分能够经过罕见的美景、簇新的视角、意外的场景、强反差的组合形式,来勾起用户满足多的好奇心,并且产生能看下去的爱好。 视觉影响带来的好奇心往往激起用户的向往,开头是普普通通的画面,还是充溢视觉影响的画面,播映完成率会有天差地别。相同的也会对于传播带来很大的影响。 5、代入产生共鸣 大家都知道“刻板形象”并不是一个好的词汇,但如果拿刻板形象自嘲却很简单打动这届深受刻板形象祸患的用户。比如说“没见过雪的南方人”就很简单火爆起来。 以上便是总结的关于如何打造出汽车行业短视频爆款短视频的相关信息,如果想了解一下靠谱的代运营公司也能够挑选和我们进行协作交流。

    2021年5月31日
    11
  • 手机信号最好的手机排名(手机信号的强弱与什么有关)

    平时我们在使用手机的时候,都希望自己的手机是一台速度很快、拍照清晰、信号很好的手机。手机运行速度和拍照效果我们知道与手机硬件及软件系统优化有关。那手机信号的好坏与什么有关呢? 手机信号重要性不言而喻,我就曾经碰到这样的情况,一次在外面出差的时候,接到一客户重要电话。我说马上处理。结果挂完电话,打开数据连接,信号链接不到,网页一直在哪转圈圈。急得人满头大汗,真的太耽误事儿了。今天就让我们来探讨一下手机信号的问题。 手机信号 手机信号如果不好,就会出现手机通话不畅,时断时续。别人的手机在电梯里可以刷视频,自己连打电话都做不到,更别提上网了。明明手机开启数据流量却出现断网的情况。特别是在比较偏僻的乡村或山区,手机信号弱就比较常见了。其实手机信号的强弱,大体由三个方面的原因决定。手机处理器芯片、手机天线还有运营商网络。 华为手机 一:手机处理器芯片,基带是关键 基带是影响手机信号强度的主要因素。当前市场上,比较主流的芯片有高通骁龙系列,联发科天玑系列,华为麒麟芯片以及苹果A系列。通常而言,同级别手机信号差不多,但是外挂基带和Soc集成基带还是有差距的。比如我们的麒麟9000就是5nm工艺制程的集成式5G Soc,还是全球首款5G移动芯片,性能非常强悍。苹果机虽然总体而言性能不错,但单就信号而言并不突出,它并没有内置基带,所有基带都是外挂在手机内的,比如苹果A14就是外挂高通X55 5G基带,包括刚刚发布的A15系列。 5G基站 前段时间,有传言iPhone 13或成史上信号最好的苹果手机。原因是它将支持地轨道卫星通信,简单来说,就是在手机没有基站信号支持的情况下,可以自动转入卫星通信。将成为首家搭载卫星通话功能的智能手机厂商。结果今天通过iPhone 13的发布会,但是却对传闻中的卫星通信功能只字未提。看来苹果手机搭载卫星通信暂时都是传言罢了! 5G信号 二:手机天线 手机天线的作用也是很重要的。原来是外置的,大家可能对大哥大记忆犹新,显得特别的霸气。现在的手机天线都看不到变成内置的了,我们已经看不到了。据悉中兴Axon30 5G特别搭载一套超级天线3.1系统,用wifi6配合网络算法,智能识别网络,可以做到WiFi与5G的无缝切换也称之为“防抱死”天线系统。华为mate30 pro就搭载了21根天线,来保证信号的稳定。同时华为还创新性地推出天线绕电池侧垂直布局方式来提高通…

    2022年5月30日
    98
  • 下载的视频播放不了怎么解决

    今天跟大家探讨的问题是:【下载的视频播放不了怎么办?】 当我们在网上下载的视频播放不了,怎么办?今天就教大家一个老司机常用的方法,还不快拿走! 它就是:迅捷视频转换器,一个专业好用的工具,支持多种音视频格式的转换,同时提供视频合并、分割等功能,轻轻松松把复杂格式转成兼容性一流的MP4! 软件支持MOV、MKV、AVI、MP4、WMV、VOB等多种格式互转,可以实现1:1高清无损转换。 转换时还可以根据所用机型,调整分辨率、视频编码,自定义添加常用的模板。 设置常用模板的作用,除了提升分辨率,最明显的就是对视频黑边的处理。 因为视频尺寸和机型的不匹配,我们下载的视频通常无法满屏观看,反正我是觉得全屏也没啥用。 如果你想要去除黑边也很简单。只需在转换时,点击【设备】列表,添加自己手机的屏幕参数,就能一键体验全屏啦。 还有一些超清影视剧自带多轨字幕,一部电影就能让你的手机内存开始提醒。 打开软件内置的【视频压缩】功能,手动输入预计大小,适当调整帧率来减少“占地面积”。 如果你需要转换大量视频格式,可千万别浪费时间一个个转换。直接把待处理的视频打包上传,选择好转换格式,一键就能快速转换! 除此以外,软件还支持音频转换、视频合并、添加字幕、屏幕录像等超多功能,这里就不一一演示了,感兴趣的小伙伴自己上手体验吧! 精彩尾巴: 以上就是今天的分享内容,如果觉得还不错,可以点赞告诉我,我会继续分享更多实用的内容。 还在烦恼下载的视频播放不了?这个老司机常用的方法,还不快拿走!

    2022年4月10日
    44
  • 微信、QQ等账号可以继承吗?

    【微信、QQ等账号可以继承吗?】互联网时代,网络账号已经成为我们生活中密不可分的一部分,但如果有一天人不在了,留下来的微信、QQ等账号能不能继承?长期不使用会被如何处理?法报君说根据民法典规定,只要是个人的合法财产就可以继承,虚拟财产也算。但根据其性质可能不能继承的除外。也就是说,社交账户上的资金余额,比如转入的资金、充值等,属于用户的私人财产,可依据法律的规定予以继承。但对于社交账户、密码等数字信息,具有人身性质,属于个人信息,不能继承。(全媒体记者 罗聪冉)

    2021年6月19日
    25
  • 微信聊天记录如何取证?

    微信是当下 最为常用的通讯工具 什么样的微信记录 才能作为有效的证据呢? 随便截几张图 就能当作呈堂证供吗? 今天小编就来给大家 科普一下 根据微信记录形成的方式,微信证据分为文字微信记录、图片微信记录、语音微信记录、视频微信记录、网络连接和转账支付信息。 1. 文字微信记录。 包括微信好友聊天、微信朋友圈发布的文字、发送的文本文件以及微信公众号发布的文章等以文字形式存在的信息。例如常见的“微信借条”。 2. 图片微信记录。 包括在与微信好友聊天、发布微信朋友圈和微信公众号时转载、制作、拍摄的图片以及使用的各类表情。 3. 语音微信记录。 包括与微信好友聊天、发布的微信朋友圈和微信公众号文章中以语音形式存在的信息。 4. 视频微信记录。 包括与微信好友聊天过程中、发表微信朋友圈和微信公众号时,转载、制作、拍摄的视频。 5. 网络链接记录。 包括与微信好友聊天过程中、发表微信朋友圈和微信公众号时发送的网络链接,此类微信记录的最大不同是链接的内容是提前由第三方或者发送方制作的。(内容具有不确定性,一般不得作为证据使用) 6. 使用支付、转账、红包功能时产生的支付转账信息 ,这一微信证据类型主要在使用微信支付功能时产生。 虽然法律对证据的形式和程序 已有较清晰的规定 但实际生活中 很多人并不熟悉具体操作方式 那么我们该如何保存微信证据呢? 【一】 提交微信相关证据时,要注意什么? 1. 提供使用终端设备登陆 本方微信账户的过程演示 。用于证明其持有微信聊天记录的合法性和本人身份的真实性。 2. 提供聊天双方的个人信息界面 。借助微信号不可更改的特点,并结合个人信息界面中显示的手机号码、头像等信息,固定双方当事人的真实身份。 3. 提供完整的聊天记录 。根据微信聊天记录在使用终端中只能删除不能添加的特点,对双方各自微信客户端完整聊天信息进行对比,以验证相关信息的完整性和真实性。 【二】 法庭上如何展示微信证据? 在法庭上,法官要求出示微信的原始载体 、登录软件出示电子证据 时,应按以下步骤进行展示,并与固定电子证据形成的图片、音频、视频进行一致性核对: 1. 由账户持有人登录微信,展示登录所使用的账户名称。 2.在通讯录中查找对方用户并点击查看个人信息,展示个人信息界面显示的备注名称、昵称、微信号、手机号等具有身份指向性的内容。 3.在个人信息界面点击“发消息”进入通讯对话…

    2021年6月13日
    17