前言
大學的學習時光臨近尾聲,感嘆時光匆匆,三年一晃而過。同學們都忙著找工作,我也在這里拋一份簡歷吧,歡迎各位老板和獵手誠邀。我們進入正題。直播行業是當前火熱的行業,誰都想從中分得一杯羹,直播養活了一大批人,一個平臺主播粗略估計就有幾千號人,但是實時在線觀看量有的居然到了驚人的百萬級別,特別是游戲主播,可想而知,直播間是一個磁鐵式的廣告傳播媒介,也難怪這么多巨頭公司都搶著做直播。我不太清楚直播行業技術有多深,畢竟自己沒做過,但是咱們可以自己實現一個滿足幾百號人同時觀看的直播間呀。
最終成果
手機端效果
這個場景很熟悉吧~~ 通過obs推流軟件來推流。
戶外直播,通過yasea手機端推流軟件,使用手機攝像頭推流。
電腦端效果
播放香港衛視
直播畫面
項目總覽
項目分為三個部分:
客戶端直播間視頻拉流、播放和聊天室,炫酷的彈幕以及直播間信息
服務端處理直播間、用戶的數據業務,聊天室消息的處理
服務器部署視頻服務器和web服務器
技術棧
移動客戶端
VUE全家桶
UI層vonic
axios
視頻播放器: vue-video-player + videojs-contrib-hls
websocket客戶端: vue-stomp
彈幕插件: vue-barrage
打包工具:webpack
電腦端客戶端
項目架構: Jquery + BootStrap
視頻播放器: video.js
websocket客戶端: stomp.js + sockjs.js
彈幕插件: Jquery.danmu.js
模版引擎: thymeleaf
服務端
IDE: IntelliJ IDEA
項目架構: SpringBoot1.5.4 +Maven3.0
主數據庫: Mysql5.7
輔數據庫: redis3.2
數據庫訪問層: spring-boot-starter-data-jpa + spring-boot-starter-data-redis
websocket: spring-boot-starter-websocket
消息中間件: RabbitMQ/3.6.10
服務器部署
視頻直播模塊: nginx-rtmp-module
web應用服務器: tomcat8.0
服務器: 騰訊云centos6.5
技術點講解
直播間主要涉及到兩個主要功能:第一是視頻直播、第二是聊天室。這兩個都是非常講究實時性。
視頻直播
說到直播我們先了解下幾個常用的直播流協議,看了挺多的流媒體協議文章博客,但都是非常粗略,這里有個比較詳細的流媒體協議介紹,如果想詳細了解協議內容估計去要看看專業書籍了。這里我們用到的只是rtmp和hls,實踐后發現:rtmp只能夠在電腦端播放,hls只能夠在手機端播放。而且rtmp是相當快的盡管沒有rtsp那么快,延遲只有幾秒,我測試的就差不多2-5秒,但是hls大概有10幾秒。所以如果你體驗過demo,就會發現手機延遲比較多。
直播的流程:
直播分為推流和拉流兩個過程,那么流推向哪里,拉流又從哪里拉取呢?那當然需要視頻服務器啦,千萬不要以為視頻直播服務器很復雜,其實在nginx服務器中一切都變得簡單。后面我會講解如何部署Nginx服務器并配置視頻模塊(nginx-rtmp-module).
首先主播通過推流軟件,比如OBS Studio推流軟件,這個是比較專業級別的,很多直播平臺的推薦主播使用這個軟件來推送視頻流,這里我也推薦一個開源的安卓端推流工具Yasea,下載地址,文件很小,但是很強大。
直播內容推送到服務器后,就可以在服務器端使用視頻編碼工具進行轉碼了,可以轉換成各種高清,標清,超清的分辨率視頻,也就是為什么我們在各個視頻網站都可以選擇視頻清晰度。這里我們沒有轉碼,只是通過前端視頻播放器(video.js)來拉取視頻.這樣整個視頻推流拉流過程就完成了。
聊天室
直播間里面的聊天室跟我們的群聊天差不多,只不過它變成了web端,web端的即時通信方案有很多,這里我們選擇websocket協議來與服務端通信,websocket是基于http之上的傳輸協議,客戶端向服務端發送http請求,并攜帶Upgrade:websocket升級頭信息表示轉換websocket協議,通過與服務端握手成功后就可以建立tcp通道,由此來傳遞消息,它與http最大的差別就是,服務端可以主動向客戶端發送消息。
既然建立了消息通道,那我們就需要往通道里發消息,但是總得需要一個東西來管控消息該發給誰吧,要不然全亂套了,所以我們選擇了消息中間件RabbitMQ.使用它來負責消息的路由去向。
理論知識都講完啦,實操時間到!
移動客戶端實操
工程結構
|—— build? ? ? ? ? ? ? ? ? ? ? ? 構建服務和webpack配置
|—— congfig? ? ? ? ? ? ? ? ? ? ? 項目不同環境的配置|—— dist? ? ? ? ? ? ? ? ? ? ? ? build生成生產目錄
|—— static? ? ? ? ? ? ? ? ? ? ? 靜態資源|—— package.json? ? ? ? ? ? ? ? 項目配置文件
|—— src? ? ? ? ? ? ? ? ? ? ? ? ? 開發源代碼目錄|—— api? ? ? ? ? ? ? ? ? ? ? 通過axios導出的api目錄
|—— components? ? ? ? ? ? ? 頁面和組件|—— public? ? ? ? ? ? ? ? ? 公有組件
|—— vuex? ? ? ? ? ? ? ? ? ? 全局狀態|—— main.js? ? ? ? ? ? ? ? ? 應用啟動配置點
功能模塊
拉取服務器的直播視頻流(hls)并播放直播畫面
與服務端創建websocket連接,收發聊天室消息
通過websocket獲取消息并發送到彈幕
通過websocket實時更新在線用戶
結合服務端獲取訪問歷史記錄
問題反饋模塊
效果圖
項目說明
服務端實操
由于個人比較喜歡接觸新的東西,所以后端選擇了springboot,前端選擇了Vue.js年輕人嘛總得跟上潮流。SpringBoot實踐過后發現真的太省心了,不用再理會各種配置文件,全自動化裝配。
這里貼一下pom.xml
4.0.0com.hushangjiertmp-demo0.0.1-SNAPSHOTjarrtmp-demoDemo project for Spring Bootorg.springframework.bootspring-boot-starter-parent1.5.4.RELEASEUTF-8UTF-81.8org.springframework.bootspring-boot-devtoolstrueorg.springframework.bootspring-boot-starter-actuatororg.springframework.bootspring-boot-actuator-docsorg.springframework.bootspring-boot-starter-data-jpaorg.springframework.bootspring-boot-starter-data-redis-->
org.springframework.boot
spring-boot-starter-security
-->org.springframework.bootspring-boot-starter-thymeleafnet.sourceforge.nekohtmlnekohtml1.9.22org.springframework.bootspring-boot-starter-web-->
org.springframework.boot
spring-boot-starter-tomcat
-->-->
javax.servlet
javax.servlet-api
3.1.0
provided
-->org.springframework.bootspring-boot-starter-websocketorg.springframework.bootspring-boot-starter-testtestorg.webjarsvue2.1.3mysqlmysql-connector-javajoda-timejoda-time2.9.2io.projectreactorreactor-core2.0.8.RELEASEio.projectreactorreactor-net2.0.8.RELEASEio.nettynetty-all4.1.6.Finalorg.springframework.bootspring-boot-maven-plugintrue
application.properties文件
spring.datasource.url=jdbc:mysql://host:3306/database?characterEncoding=utf8&useSSL=falsespring.datasource.username=usernamespring.datasource.password=passwordspring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.thymeleaf.mode=LEGACYHTML5server.port=8085# REDIS (RedisProperties)# Redis數據庫索引(默認為0)spring.redis.database=0# Redis服務器地址spring.redis.host=127.0.0.1# Redis服務器連接端口spring.redis.port=6379# Redis服務器連接密碼(默認為空)spring.redis.password=# 連接池最大連接數(使用負值表示沒有限制)spring.redis.pool.max-active=8# 連接池最大阻塞等待時間(使用負值表示沒有限制)spring.redis.pool.max-wait=-1# 連接池中的最大空閑連接spring.redis.pool.max-idle=8# 連接池中的最小空閑連接spring.redis.pool.min-idle=0# 連接超時時間(毫秒)spring.redis.timeout=0
websocket配置
@Configuration@EnableWebSocketMessageBrokerpublicclassWebSocketConfigextendsAbstractWebSocketMessageBrokerConfigurer{//攔截器注入service失敗解決辦法@BeanpublicMyChannelInterceptormyChannelInterceptor(){returnnewMyChannelInterceptor();? ? }@OverridepublicvoidregisterStompEndpoints(StompEndpointRegistry registry){//添加訪問域名限制可以防止跨域socket連接//setAllowedOrigins("http://localhost:8085")registry.addEndpoint("/live").setAllowedOrigins("*").addInterceptors(newHandShkeInceptor()).withSockJS();? ? }@OverridepublicvoidconfigureMessageBroker(MessageBrokerRegistry registry){/*.enableSimpleBroker("/topic","/queue");*///假如需要第三方消息代理,比如rabitMQ,activeMq,在這里配置registry.setApplicationDestinationPrefixes("/demo")? ? ? ? ? ? ? ? .enableStompBrokerRelay("/topic","/queue")? ? ? ? ? ? ? ? .setRelayHost("127.0.0.1")? ? ? ? ? ? ? ? .setRelayPort(61613)? ? ? ? ? ? ? ? .setClientLogin("guest")? ? ? ? ? ? ? ? .setClientPasscode("guest")? ? ? ? ? ? ? ? .setSystemLogin("guest")? ? ? ? ? ? ? ? .setSystemPasscode("guest")? ? ? ? ? ? ? ? .setSystemHeartbeatSendInterval(5000)? ? ? ? ? ? ? ? .setSystemHeartbeatReceiveInterval(4000);? ? }@OverridepublicvoidconfigureClientInboundChannel(ChannelRegistration registration){? ? ? ? ChannelRegistration channelRegistration = registration.setInterceptors(myChannelInterceptor());super.configureClientInboundChannel(registration);? ? }@OverridepublicvoidconfigureClientOutboundChannel(ChannelRegistration registration){super.configureClientOutboundChannel(registration);? ? }}
配置類繼承了消息代理配置類,意味著我們將使用消息代理rabbitmq.使用registerStompEndpoints方法注冊一個websocket終端連接。這里我們需要了解兩個東西,第一個是stomp和sockjs,sockjs是啥呢,其實它是對于websocket的封裝,因為如果單純使用websocket的話效率會非常低,我們需要的編碼量也會增多,而且如果瀏覽器不支持websocket,sockjs會自動降級為輪詢策略,并模擬websocket,保證客戶端和服務端可以通信。
stomp有是什么看這里
stomp是一種簡單(流)文本定向消息協議,它提供了一個可互操作的連接格式,允許STOMP客戶端與任意STOMP消息代理(Broker)進行交互,也就是我們上面的RabbbitMQ,它就是一個消息代理。
我們可以通過configureMessageBroker來配置消息代理,需要注意的是我們將要部署的服務器也應該要有RabbitMQ,因為它是一個中間件,安裝非常容易,這里就不說明了。這里我們配置了“/topic,/queue”兩個代理轉播策略,就是說客戶端訂閱了前綴為“/topic,/queue”頻道都會通過消息代理(RabbitMQ)來轉發。跟spring沒啥關系啦,完全解耦。
websocke如何保證安全
一開始接觸 stomp的時候一直有個問題困擾我,客戶端只要與服務端通過websocket建立了連接,那么他就可以訂閱任何內容,意味著可以接受任何消息,這樣豈不是亂了套啦,于是我翻閱了大量博客文章,很多都是官方的例子并沒有解決實際問題。經過琢磨,其實websocket是要考慮安全性的。具體在以下幾個方面
跨域websocket連接
協議升級前握手攔截器
消息信道攔截器
對于跨域問題,我們可以通過setAllowedOrigins方法來設置可連接的域名,防止跨站連接。
對于站內用戶是否允許連接我們可以如下配置
publicclassHandShkeInceptorextendsHttpSessionHandshakeInterceptor{privatestaticfinalSetONLINE_USERS=newHashSet<>();@Overridepublic boolean beforeHandshake(ServerHttpRequestrequest,ServerHttpResponseresponse,WebSocketHandlerwsHandler,Map attributes)throwsException{System.out.println("握手前"+request.getURI());//http協議轉換websoket協議進行前,通常這個攔截器可以用來判斷用戶合法性等//鑒別用戶if(request instanceofServletServerHttpRequest) {ServletServerHttpRequestservletRequest = (ServletServerHttpRequest) request;//這句話很重要如果getSession(true)會導致移動端無法握手成功//request.getSession(true):若存在會話則返回該會話,否則新建一個會話。//request.getSession(false):若存在會話則返回該會話,否則返回NULL//HttpSession session = servletRequest.getServletRequest().getSession(false);HttpSessionsession = servletRequest.getServletRequest().getSession();UserEntityuser = (UserEntity) session.getAttribute("user");if(user !=null) {//這里只使用簡單的session來存儲用戶,如果使用了springsecurity可以直接使用principalreturnsuper.beforeHandshake(request, response, wsHandler, attributes);? ? ? ? ? ? }else{System.out.println("用戶未登錄,握手失敗!");returnfalse;? ? ? ? ? ? }? ? ? ? }returnfalse;? ? }@Overridepublic void afterHandshake(ServerHttpRequestrequest,ServerHttpResponseresponse,WebSocketHandlerwsHandler,Exceptionex) {//握手成功后,通常用來注冊用戶信息System.out.println("握手后");super.afterHandshake(request, response, wsHandler, ex);? ? }}
HttpSessionHandshakeInterceptor 這個攔截器用來管理握手和握手后的事情,我們可以通過請求信息,比如token、或者session判用戶是否可以連接,這樣就能夠防范非法用戶。
那如何限制用戶只能訂閱指定內容呢?我們接著往下看
publicclassMyChannelInterceptorextendsChannelInterceptorAdapter{@Autowiredprivate StatDao statDao;@Autowiredprivate SimpMessagingTemplate simpMessagingTemplate;@Overridepublic boolean preReceive(MessageChannel channel) {? ? ? ? System.out.println("preReceive");returnsuper.preReceive(channel);? ? }@Overridepublic Message preSend(Message message, MessageChannel channel) {? ? ? ? StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);? ? ? ? StompCommand command = accessor.getCommand();//檢測用戶訂閱內容(防止用戶訂閱不合法頻道)if(StompCommand.SUBSCRIBE.equals(command)) {//從數據庫獲取用戶訂閱頻道進行對比(這里為了演示直接使用set集合代替)Set subedChannelInDB =newHashSet<>();? ? ? ? ? ? subedChannelInDB.add("/topic/group");? ? ? ? ? ? subedChannelInDB.add("/topic/online_user");if(subedChannelInDB.contains(accessor.getDestination())) {//該用戶訂閱的頻道合法returnsuper.preSend(message, channel);? ? ? ? ? ? }else{//該用戶訂閱的頻道不合法直接返回null前端用戶就接受不到該頻道信息。returnnull;? ? ? ? ? ? }? ? ? ? }else{returnsuper.preSend(message, channel);? ? ? ? }? ? }@OverridepublicvoidafterSendCompletion(Message message, MessageChannel channel, boolean sent, Exception ex) {//System.out.println("afterSendCompletion");//檢測用戶是否連接成功,搜集在線的用戶信息如果數據量過大我們可以選擇使用緩存數據庫比如redis,//這里由于需要頻繁的刪除和增加集合內容,我們選擇set集合來存儲在線用戶StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);? ? ? ? StompCommand command = accessor.getCommand();if(StompCommand.SUBSCRIBE.equals(command)){Map map = (Map) accessor.getHeader("simpSessionAttributes");//ONLINE_USERS.add(map.get("user"));UserEntity user = map.get("user");if(user !=null){? ? ? ? ? ? ? ? statDao.pushOnlineUser(user);? ? ? ? ? ? ? ? Guest guest =newGuest();? ? ? ? ? ? ? ? guest.setUserEntity(user);? ? ? ? ? ? ? ? guest.setAccessTime(Calendar.getInstance().getTimeInMillis());? ? ? ? ? ? ? ? statDao.pushGuestHistory(guest);//通過websocket實時返回在線人數this.simpMessagingTemplate.convertAndSend("/topic/online_user",statDao.getAllUserOnline());? ? ? ? ? ? }? ? ? ? }//如果用戶斷開連接,刪除用戶信息if(StompCommand.DISCONNECT.equals(command)){Map map = (Map) accessor.getHeader("simpSessionAttributes");//ONLINE_USERS.remove(map.get("user"));UserEntity user = map.get("user");if(user !=null){? ? ? ? ? ? ? ? statDao.popOnlineUser(user);? ? ? ? ? ? ? ? simpMessagingTemplate.convertAndSend("/topic/online_user",statDao.getAllUserOnline());? ? ? ? ? ? }? ? ? ? }super.afterSendCompletion(message, channel, sent, ex);? ? }}
在stomp里面,Channel信道就是消息傳送的通道,客戶端與服務端建立了連接就相當于建立了通道,以后的信息就是通過這個通道來傳輸。所有的消息都有消息頭,被封裝在了spring 的messag接口中,比如建立連接時候消息頭就含有CONNECT,當然還有一些其他的信息。客戶端訂閱的時候也有訂閱頭信息SUBSCRIBE,那么我是不是可以在這個攔截器ChannelInterceptorAdapter 中攔截每個人的訂閱信息,然后與數據庫的信息作比對,最后決定這個用戶是否可以訂閱這個頻道的信息呢,對的,這是我的想法,按照這樣的思路,做單聊不是迎刃而解了嗎。
那客戶端通過websocket發送的消息如何到達訂閱者手中呢,按照rabbitmq的規則,訂閱者屬于消費者,發送消息的一方屬于生產者,生產者通過websocket把消息發送到服務端,服務端通過轉發給消息代理(rabbitmq),消息代理負責存儲消息,管理發送規則,推送消息給訂閱者,看下面的代碼
@MessageMapping(value ="/chat")@SendTo("/topic/group")? ? public MsgEntity testWst(Stringmessage ,@Header(value ="simpSessionAttributes")Map session){? ? ? ? UserEntity user = (UserEntity) session.get("user");Stringusername = user.getRandomName();? ? ? ? MsgEntity msg =newMsgEntity();? ? ? ? msg.setCreator(username);? ? ? ? msg.setsTime(Calendar.getInstance());? ? ? ? msg.setMsgBody(message);returnmsg;? ? }
@MessageMapping看起來跟springmvc方法特別像,它即可以用在類級別上也可以用在方法級別上
當發送者往‘/chat’發送消息后,服務端接受到消息,再發送給“/topic/group”的訂閱者,@SendTo就是發送給誰,這里需要注意的有,如果我們沒有配置消息代理,只使用了enableSimpleBroker("/topic","/queue")簡單消息代理,那么就是直接發送到消息訂閱者,如果配置了消息代理,那還要通過消息代理,由它來轉發。
如果我們想在服務端隨時發送消息,而不是在客戶端發送(這樣的場景很常見,比如發送全局通知),可以使用SimpMessagingTemplate類,通過注入該bean,在合適的業務場景中發送消息。
Redis統計數據
直播間經常需要統計數據,比如實時在線人數,訪問量,貢獻排行榜,訂閱量。我選擇的方案是使用redis來計數,盡管這個demo可能不會太多人訪問,但是我的目的是學習如何使用redis
先看springboot中redis的配置
@ConfigurationpublicclassRedisConfigextendsCachingConfigurerSupport{/*** 生成key的策略** @return*/@Beanpublic KeyGenerator keyGenerator() {returnnewKeyGenerator() {@OverridepublicObjectgenerate(Objecttarget, Method method,Object... params) {? ? ? ? ? ? ? ? StringBuilder sb =newStringBuilder();? ? ? ? ? ? ? ? sb.append(target.getClass().getName());? ? ? ? ? ? ? ? sb.append(method.getName());for(Objectobj : params) {? ? ? ? ? ? ? ? ? ? sb.append(obj.toString());? ? ? ? ? ? ? ? }returnsb.toString();? ? ? ? ? ? }? ? ? ? };? ? }/*** 管理緩存** @param redisTemplate* @return*/@SuppressWarnings("rawtypes")@Beanpublic CacheManager cacheManager(RedisTemplate redisTemplate) {? ? ? ? RedisCacheManager rcm =newRedisCacheManager(redisTemplate);//設置緩存過期時間// rcm.setDefaultExpiration(60);//秒//設置value的過期時間Map map=newHashMap();? ? ? ? map.put("test",60L);? ? ? ? rcm.setExpires(map);returnrcm;? ? }/*** RedisTemplate配置* @param factory* @return*/@Beanpublic RedisTemplate redisTemplate(RedisConnectionFactoryfactory) {? ? ? ? StringRedisTemplate template =newStringRedisTemplate(factory);? ? ? ? Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =newJackson2JsonRedisSerializer(Object.class);? ? ? ? ObjectMapper om =newObjectMapper();? ? ? ? om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);? ? ? ? om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);? ? ? ? jackson2JsonRedisSerializer.setObjectMapper(om);? ? ? ? template.setValueSerializer(jackson2JsonRedisSerializer);//如果key是String 需要配置一下StringSerializer,不然key會亂碼 /XX/XXtemplate.afterPropertiesSet();//template.setStringSerializer();returntemplate;? ? }}
redis數據統計Dao的實現
@Repositorypublic class StatDao {@AutowiredRedisTemplate redisTemplate;publicvoidpushOnlineUser(UserEntity userEntity){redisTemplate.opsForSet().add("OnlineUser",userEntity);? ? }publicvoidpopOnlineUser(UserEntity userEntity){redisTemplate.opsForSet().remove("OnlineUser",userEntity);? ? }publicSetgetAllUserOnline(){returnredisTemplate.opsForSet().members("OnlineUser");? ? }publicvoidpushGuestHistory(Guest guest){//最多存儲指定個數的訪客if(redisTemplate.opsForList().size("Guest") ==200l){redisTemplate.opsForList().rightPop("Guest");? ? ? ? }redisTemplate.opsForList().leftPush("Guest",guest);? ? }publicListgetGuestHistory(){returnredisTemplate.opsForList().range("Guest",0,-1);? ? }}
Dao層非常簡單,因為我們只需要統計在線人數和訪客。但是在線人數是實時更新的,既然我們使用了websocket實時數據更新就非常容易了,前面我們講過,通過信道攔截器可以攔截連接,訂閱,斷開連接等等事件信息,所以我們就可以當用戶連接時存儲在線用戶,通過websocket返回在線用戶信息。
publicclassMyChannelInterceptorextendsChannelInterceptorAdapter{@Autowiredprivate StatDao statDao;@Autowiredprivate SimpMessagingTemplate simpMessagingTemplate;@Overridepublic boolean preReceive(MessageChannel channel) {? ? ? ? System.out.println("preReceive");returnsuper.preReceive(channel);? ? }@Overridepublic Message preSend(Message message, MessageChannel channel) {? ? ? ? StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);? ? ? ? StompCommand command = accessor.getCommand();//檢測用戶訂閱內容(防止用戶訂閱不合法頻道)if(StompCommand.SUBSCRIBE.equals(command)) {//從數據庫獲取用戶訂閱頻道進行對比(這里為了演示直接使用set集合代替)Set subedChannelInDB =newHashSet<>();? ? ? ? ? ? subedChannelInDB.add("/topic/group");? ? ? ? ? ? subedChannelInDB.add("/topic/online_user");if(subedChannelInDB.contains(accessor.getDestination())) {//該用戶訂閱的頻道合法returnsuper.preSend(message, channel);? ? ? ? ? ? }else{//該用戶訂閱的頻道不合法直接返回null前端用戶就接受不到該頻道信息。returnnull;? ? ? ? ? ? }? ? ? ? }else{returnsuper.preSend(message, channel);? ? ? ? }? ? }@OverridepublicvoidafterSendCompletion(Message message, MessageChannel channel, boolean sent, Exception ex) {//System.out.println("afterSendCompletion");//檢測用戶是否連接成功,搜集在線的用戶信息如果數據量過大我們可以選擇使用緩存數據庫比如redis,//這里由于需要頻繁的刪除和增加集合內容,我們選擇set集合來存儲在線用戶StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);? ? ? ? StompCommand command = accessor.getCommand();if(StompCommand.SUBSCRIBE.equals(command)){Map map = (Map) accessor.getHeader("simpSessionAttributes");//ONLINE_USERS.add(map.get("user"));UserEntity user = map.get("user");if(user !=null){? ? ? ? ? ? ? ? statDao.pushOnlineUser(user);? ? ? ? ? ? ? ? Guest guest =newGuest();? ? ? ? ? ? ? ? guest.setUserEntity(user);? ? ? ? ? ? ? ? guest.setAccessTime(Calendar.getInstance().getTimeInMillis());? ? ? ? ? ? ? ? statDao.pushGuestHistory(guest);//通過websocket實時返回在線人數this.simpMessagingTemplate.convertAndSend("/topic/online_user",statDao.getAllUserOnline());? ? ? ? ? ? }? ? ? ? }//如果用戶斷開連接,刪除用戶信息if(StompCommand.DISCONNECT.equals(command)){Map map = (Map) accessor.getHeader("simpSessionAttributes");//ONLINE_USERS.remove(map.get("user"));UserEntity user = map.get("user");if(user !=null){? ? ? ? ? ? ? ? statDao.popOnlineUser(user);? ? ? ? ? ? ? ? simpMessagingTemplate.convertAndSend("/topic/online_user",statDao.getAllUserOnline());? ? ? ? ? ? }? ? ? ? }super.afterSendCompletion(message, channel, sent, ex);? ? }}
由于這個項目有移動端和電腦端,所以需要根據請求代理UserAgent來判斷客戶端屬于哪一種類型。這個工具類在源碼上有。我就不貼了。
服務器部署
說了這么多即時通信,卻沒發現視頻直播。不要著急我們馬上進入視頻環節。文章開頭就說明了幾種媒體流協議,這里不講解詳細的協議流程,只需要知道,我們是通過推流軟件采集視頻信息,如何采集也不是我們關注的。采集到信息后通過軟件來推送到指定的服務器,如下圖
obs推流設置
yasea手機端推流設置
紅色部分是服務器開放的獲取流接口。
Nginx-rtmp-module配置
視頻服務器有很多,也支持很多媒體流協議。這里我們選擇nginx-rtmp-module來做視頻服務,接下來我們需要在linux下安裝nginx,并安裝rtmp模塊。本人也是linux初學者,一步步摸索著把服務器搭建好,聽說tomcat和nginx很配哦,所以作為免費開源的當然首選這兩個。
接下來需要在linux安裝一下軟件和服務。
Nginx以及Nginx-rtmp-module
Tomcat
Mysql
Redis
RabbitMQ
安裝步驟我就不說了,大家搜索一下啦,這里貼一下nginx.conf文件配置
rtmp{server{listen1935;chunk_size4096;applicationvideo {play/yjdata/www/www/video;? ? ? ? }applicationlive {liveon;hlson;hls_path/yjdata/www/www/live/hls/;hls_fragment5s;? ? ? ? }? ? }}
上面代碼是配置rtmp模塊, play /yjdata/www/www/video 指的是配置點播模塊,可以直接播放/yjdata/www/www/video路徑下的視頻。hls_path制定hls分塊存放路徑,因為hls是通過獲取到推送的視頻流信息,分塊存儲在服務器。所以它的延時比rtmp要更高。
server {? ? ? ? listen80;? ? ? ? server_name? localhost;#charset koi8-r;index index.jsp index.html;? ? ? ? root /yjdata/www/www;#access_log? logs/host.access.log? main;location / {? ? ? ? ? ? proxy_pass? http://127.0.0.1:8080;? ? ? ? }? ? ? ? location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|js|css|docx|pdf|doc|ppt|html|properties)$ {? ? ? ? ? ? ? ? expires30d;? ? ? ? ? ? ? ? root/yjdata/www/www/static/;? ? ? ? }? ? ? ? location /hls {? ? ? ? ? ? types {? ? ? ? ? ? ? ? application/vnd.apple.mpegurl m3u8;#application/x-mpegURL;video/mp2t ts;? ? ? ? ? ? }? ? ? ? ? ? alias/yjdata/www/www/live/hls/;? ? ? ? ? ? expires-1;? ? ? ? ? ? add_header Cache-Controlno-cache;? ? ? ? }? ? ? ? location /stat {? ? ? ? ? ? ? ? rtmp_stat all;? ? ? ? ? ? ? ? rtmp_stat_stylesheet stat.xsl;? ? ? ? }? ? ? ? location /stat.xsl {? ? ? ? ? ? ? ? root/soft/nginx/nginx-rtmp-module/;? ? ? ? }
上面配置了location 指向/hls,別名是/yjdata/www/www/live/hls/,所以可以在前端直接通過域名+/hls/+文件名.m3u8獲取直播視頻。
關于nginx的配置還有很多,我也在學習當中。總而言之nginx非常強大。
總結
通過從前端=>后臺=>服務器,整個流程走下來還是需要花很多心思。但是收獲也是很多。本人將從大學出來,初出茅廬,文章錯誤之處,盡請指正。