如何搭建Rtmp服務結合uni-app開發直播APP

前言:

    由於自己有一個IM類的應用,爲了完善它所以決定也加上直播和短視頻功能。做直播目前有兩種方法,一是直接對接第三方的直播服務產品,二是自己搭服務再開發。所以這裏也從這兩個方法推薦簡單的實現方式,阿里雲和騰訊雲之類的大廠產品就不安利了。(公衆號回覆“直播”獲取源碼)

 

選型:

1. 第三方,PHP+Uni-App+LiveQing

2. 自己開發,PHP+Uni-app+Nginx-rtmp-module

實現流程:

1. 客戶端採集視頻流。(開攝像頭,錄屏等)

2. 客戶端推流到rtmp服務器上。

3. rtmp推流到某個特定端口。

4. 其他客戶端再對該視頻流進行拉流,實現直播。

 

一、第三方方式

    第三方這次推薦的是一個叫LiveQing的平臺,有點是搭建快捷方便,功能完善。在服務器上運行了他們的包後除了能實現主流業務場景的直播,而且還提供短視頻的點播服務。還包括API調用,通過接口實現直播的創建,刪除,直播數據統計。但是是要收費,該軟件包在一臺物理機或雲服務器上只能免費試用一個月。

1. 找到該官網,選擇rtmp直播點播流媒體,下載試用把對應系統解壓到自己服務器。

2. 目錄如下,將start.sh授權爲777。然後./start.sh 運行該文件。

3. 運行前可以打開liveqing.ini進行設置,比如後臺登錄密碼,端口號等。

4. 默認需要開啓10080和10085,所以需要用防火牆放行,操作如下。

systemctl start firewalld.service    // 開啓防火牆

firewall-cmd add-port=10080/tcp --permanent
firewall-cmd add-port=10082/tcp --permanent

firewall-cmd --reload               // 重啓

firewall-cmd --list-ports           // 查看放行的所有端口

5. 端口放行,然後在運行start.sh出現下面圖標表示成功。

6. 瀏覽器輸入服務器的外網IP:10080,就可以進入控制面板了。

7. 創建一個直播,設置名稱和ID,然後選擇編輯獲取推流地址。

8. 爲了測試可以本地下載一個OBS軟件推流到該地址,只要一推流,直播狀態就會顯示直播中並且點擊編輯可以獲取拉流的地址。

9. 同樣爲了方便可以使用VLS軟件進行拉流或者wowza在線網站測試直播。

二、代碼實現

    不使用第三方的話,就需要搭建rtmp服務,配置Nginx,APP視頻採集推流,拉流等等。如果是大型平臺,需要進行分流集羣等。流媒體服務器依賴的服務,1.nginx 服務器;2.nginx服務器安裝需要依賴的服務 OpenSSL、pcre、zlib、 c++、gcc等,服務器環境是Centos 7.3 64 位。

1. 進入根目錄,mkdir source #創建源碼目錄,後面的源碼都放在這個目錄。cd source進入該目錄。

2. 下載git,yum -y install git,然後通過網絡下載需要的包。

git clone https://github.com/nginx/nginx.git 				#從github服務器上將nginx的源代碼下載下來
git clone https://github.com/arut/nginx-rtmp-module.git 	#將rtmp模塊的源碼下載下來
wget https://www.openssl.org/source/openssl-1.1.0.tar.gz 	#下載OpenSSL源碼包
wget https://ftp.pcre.org/pub/pcre/pcre-8.39.tar.gz 		#下載pcre源碼包
wget http://www.zlib.net/zlib-1.2.11.tar.gz 				#下載zlib包源碼

3. tar -zxvf 包名  #解壓各個包源碼

4. 在將nginx和需要的包編譯前需要先安裝gcc,安裝過可以省過。

yum -y install gcc 			#確保依賴的gcc安裝
yum -y install gcc-c++ 		#確保依賴的c++已經安裝

5. 然後cd命令進入source下的nginx目錄,輸入下面命令。

./auto/configure --prefix=/usr/local/nginx \
        --with-pcre=../pcre-8.39 \
        --with-openssl=../openssl-1.1.0 \
        --with-zlib=../zlib-1.2.11 \
        --with-http_v2_module \
        --with-http_flv_module \
        --with-http_mp4_module \
        --add-module=../nginx-rtmp-module/

6. 檢查成功會出現如下,然後make編譯一下。

7. make install 安裝

8. 以上操作後表示Nginx編譯安裝完成,然後cd到根目錄,/usr/local/nginx/sbin,如果要測試Nginx是否可以訪問。先放行80端口重啓防火牆,在sbin下輸入./nginx啓動Nginx服務。瀏覽器訪問IP地址:80,出現以下表示成功。

9. 在nginx配置文件中配置rtmp服務,記住rtmp服務是和http服務是平級,所以我們需要在和http配置平級的位置另起rtmp服務。

vi /usr/local/nginx/conf/nginx.conf #修改配置文件
rtmp  {
    server  {
        listen 1935;
        chunk_size 4096;
        application live  {
            live on;
            record off;
        }
        application live2  {
            live on;
            record off;
        }
        application vod  {
            play /var/flvs;
        }
        application vod_http  {
            play http://服務器的ip/vod;
        }
        application hls  {
            live on;
            hls on;
            hls_path /tmp/hls;
        }
    }
}
/usr/local/nginx/sbin/nginx -s reload  #修改配置文件重啓nginx服務

10. 上面rtmp服務的端口是1935,所以也需要按之前方法給1935端口放行,檢查雲服務器的安全組是否也放行,然後再重啓防火牆。

11. 本地電腦測試1935是否開啓,可以cmd命令telnet 服務器IP地址 端口號,如果出現一下界面說明端口已經通了 。

12. 接下來也可以通過OBS推流到該地址,然後用WOWZA拉流進行測試。

rtmp://你的服務器ip:端口(1935)/live #URL填寫流的地址

13. 接下來演示uni-app的推流寫法。

<template>
    <view class="content">		
		<view class="butlist">
			<view @click="back" class="buticon martp10">
				<image src="../../static/zhiwen-livepush/back2.png"></image>	
				<view class="mar10">返回</view>				
			</view>
			<view @click="switchCamera" class="buticon martp10">
				<image src="../../static/zhiwen-livepush/reversal.png"></image>	
				<view class="mar10">翻轉</view>				
			</view>
			<view class=" buticon" @click="startPusher">
				<view class="x_f"></view>
				<view :class="begin==true?'givebegin':'give'" >{{contTime}}</view>
				<view class="pulse" v-if="begin"></view>
			</view>
			<view class="buticon martp10">
				<image src="../../static/zhiwen-livepush/beautiful.png"></image>	
				<view class="mar10">美化</view>				
			</view>
			<view   class="buticon martp10" v-if="begin==false">
				<picker :value="index" @change="bindPickerChange" :range="array" range-key='cont'>
					<image src="../../static/zhiwen-livepush/countdown.png"></image>	
					<view class="mar10">倒計時</view>
				</picker>	
			</view>

			<view @click="upload" class="buticon martp10" v-if="begin">
				<image src="../../static/zhiwen-livepush/yes.png"></image>	
				<view class="mar10">完成</view>				
			</view>			
		</view>
		
		 
    </view>

</template>

<script>
    export default {
		data() {
			return {
			    begin:false,//開始錄製
				complete:false,//錄製完畢
				pause:false,//暫停推流
				currentWebview:null,
				pusher:null,
				livepushurl:'rtmp://106.52.216.244:10089/hls/1',  //這裏修改自己的推流地址就可以了
				logininfokey:'',//登錄驗證加密串,
				homeworkcont:'',//作業信息
				jiexititle:'',//作業解析標題
				index: 0,//定時
				indextu:0,//是否開啓定時
				contTime:'',
				array: [{//話題標籤
						"id": 1,
						"cont": "10秒",
						"time": 10
					}, {
						"id": 2,
						"cont": "20秒",
						"time": 20
					}, {
						"id": 3,
						"cont": "30秒",
						"time": 30
					}, {
						"id": 4,
						"cont": "40秒",
						"time": 40
					},{
						"id": 5,
						"cont": "50秒",
						"time": 50
					},
					{
						"id": 6,
						"cont": "60秒",
						"time": 60
					}],
			}
		},
		 
		onShow() {
			 uni.getNetworkType({
				success: function (res) {
					console.log(res.networkType);
					if(res.networkType != 'wifi'){
						uni.showModal({ //提醒用戶更新
							title: '溫馨提示',
							content: '當前非Wifi網絡,請注意您的流量是否夠用',
							success: (res) => {
								 
							}
						})
					}
				}
			});
			uni.onNetworkStatusChange(function (res) {
				console.log(res.isConnected);
				console.log(res.networkType);
				if(res.networkType != '4g' && res.networkType != 'wifi'){
					uni.showModal({ //提醒用戶更新
						title: '溫馨提示',
						content: '當前網絡質量差,請切換爲4G網絡或Wifi網絡',
						success: (res) => {
							 
						}
					})
				}
			});
		/* 	plus.key.addEventListener("backbutton",()=>{
				console.log("BackButton Key pressed!" );
				//this.back()
				return false
			}); */
		},
		 onBackPress(){
				this.back()
			    console.log("BackButton Key pressed!" );
				return true;
		 },
        onLoad(res) {
			console.log(res)
			this.jiexititle=res.title
			uni.getStorage({
				key: 'logininfokey',
				success:(res) =>{
					console.log(res.data);
					this.logininfokey=res.data
					console.log(this.logininfokey)
				}
			});
			uni.getStorage({
				key: 'clickworkcont',
				success:(res) =>{
					console.log(res.data);
					this.homeworkcont=res.data
					//console.log(this.logininfokey)
				}
			});
			
			uni.getStorage({
				key: 'livepushurl',
				success:(res) =>{
					console.log(res.data);
					this.livepushurl=res.data
				}
			});
			console.log(this.livepushurl)
	        this.getwebview()//獲取webview
        },
		methods: {
			//倒計時
			bindPickerChange: function(e) {
			    console.log('picker發送選擇改變,攜帶值爲', e.target.value)
			    this.index = e.target.value
				// this.indexs = e.target.value
				this.contTime=this.array[e.target.value].time
				uni.showToast({
					title: '請點擊紅色按鈕,開始進入倒計時',
					icon:'none',
					duration: 4000,					 
				});
			},
			
			/**
			 * 返回
			 */
			back(){
				uni.showModal({
					title: '提示',
					content: '返回後未上傳的視頻需要重新錄製哦',
					success: function (res) {
						if (res.confirm) {
							/* this.currentWebview=null;
							this.pusher=null */
							uni.redirectTo({
								url:'../user/issue'
							})
							//this.currentWebview=null
						} else if (res.cancel) {
							console.log('用戶點擊取消');
						}
					}
				});
				
			},
			/**
			 * 獲取當前顯示的webview
			 */
			getwebview(){
				var pages = getCurrentPages();
				var page = pages[pages.length - 1];
				// #ifdef APP-PLUS
				var getcurrentWebview = page.$getAppWebview();
				console.log(this.pages)
				console.log(this.page)
				console.log(JSON.stringify(page.$getAppWebview()))
				this.currentWebview=getcurrentWebview;
				// #endif
				this.plusReady()//創建LivePusher對象
			},

			/**
			 * 創建LivePusher對象 即推流對象
			 */ 
			plusReady(){				
				// 創建直播推流控件
				this.pusher =new plus.video.LivePusher('pusher',{
					url:'',
					top:'0',
					left:'0px',
					width: '100%',
					height:  uni.getSystemInfoSync().windowHeight-15 + 'px',				
					position: 'absolute',//static靜態佈局模式,如果頁面存在滾動條則隨窗口內容滾動,absolute絕對佈局模式,如果頁面存在滾動條不隨窗口內容滾動; 默認值爲"static"
					beauty:'0',//美顏 0-off  1-on  
					whiteness:'0',//0、1、2、3、4、5,0不使用美白,值越大美白程度越大。
					aspect:'9:16',					
 				});
				console.log(JSON.stringify(this.pusher))
				console.log(JSON.stringify(this.currentWebview))
				//將創建的對象 追加到webview中
				this.currentWebview.append(this.pusher);
				// 監聽狀態變化事件  
				this.pusher.addEventListener('statechange',(e)=>{
					console.log('statechange: '+JSON.stringify(e));
				}, false);
			},			
			//美顏
			beautiful(){
				console.log(JSON.stringify(this.pusher))
				this.pusher.options.beauty=1
				this.plusReady()//創建LivePusher對象
			},
			// 開始推流
			startPusher(){
				//判斷是否倒計時開始
				if(this.contTime!=''){
					if(this.indextu!=1){
						this.conttimejs()
					}
				}else{
					this.beginlivepush()
				}
			},
			conttimejs(){
				if(this.contTime!=''){
					this.indextu=1;//開啓計時
					if(this.contTime==1){
						console.log("開始")
						this.contTime=""
						this.beginlivepush()
						return false
					}
					this.contTime--
					setTimeout(()=>{
						this.conttimejs()
					},1000)
				}
			},
			beginlivepush() {
				this.indextu=0;//關閉計時
				if(this.begin==false){//未開啓推流
					this.begin=true;//顯示錄製動畫
					// 設置推流服務器  ***此處需要通過ajax向後端獲取
					this.pusher.setOptions({
						url:this.livepushurl //推流地址********************************* 此處設置推流地址
					});
					this.pusher.start();//推流開啓
					uni.showToast({
						title: '開始錄製',
						icon:'none',
						duration: 2000,					 
					});
				}else{
					if(this.pause==true){//暫停推流狀態
						this.begin=true;//顯示錄製動畫
						this.pause=false;//推流開關置爲默認狀態
						this.pusher.resume();//恢復推流
						uni.showToast({
							title: '開始錄製',
							icon:'none',
							duration: 2000,					 
						});
					}else{
						this.begin=false;//關閉錄製動畫
						this.pause=true;//推流暫停
						this.pusher.pause();;//暫停推流
						uni.showToast({
							title: '暫停錄製',
							icon:'none',
							duration: 2000,					 
						});
						//提示是否上傳
						this.upload()
						
						
					}

				}
			},
			/**
			 * 切換攝像頭
			 */ 
			switchCamera() {
				this.pusher.switchCamera();
			},
			/**
			 * 完成錄製
			 */
			upload(){
				 uni.showModal({
				 	title: '提示',
				 	content: '確定保存嗎',
				 	success:(res)=> {
				 		if (res.confirm) {
				 			 console.log('用戶點擊完成');
							 this.pusher.pause();;//暫停推流
							 this.endlivepush()
							 
							/* setTimeout(()=>{
								 this.endlivepush()
							 },1000) */
				 		} else if (res.cancel) {
				 			console.log('用戶點擊取消');
				 		}
				 	}
				 });
			}, 
			//結束推流,此處需要調用後臺接口向雲服務商提交結束狀態
			endlivepush(){
					uni.showToast({
					icon:'loading',
					title: '結束...',
					duration: 5000
				});
				return false
				uni.request({
						url: "",    	
				       	method: 'POST',
						// dataType:'JSON',
				       data:{},
				       success:(res)=>{
						   console.log(JSON.parse(res.data))
						   console.log(JSON.stringify(res.data))
							uni.showToast({
								icon:'loading',
								title: '視頻上傳中...',
								duration: 5000
							});
							
							setTimeout(()=>{							
								uni.showToast({
									icon:'none',
									title: '上傳完成',
									duration: 2000
								});
							},5000)
							setTimeout(()=>{							
								uni.redirectTo({
									url: 'setvideotit?id='+this.homeworkcont.id,
								});
							},7000)
				       },
				       error: (data)=>{
				       	//alert(JSON.stringify(data)+'錯誤')			    
				       }
				   });
			},
			 
		},
		components:{
		
		}
    }
</script>

<style>
	.content{
		background: #000;
		overflow: hidden;
	}
	.butlist{
		height: 140upx;
		position: absolute;
		bottom: 0;
		display: flex;
		width: 100%;
		justify-content: space-around;
	    padding-top: 20upx;
		border-top: 1px solid #fff;
		background: #000;
	}
	.buticon{
		height: 120upx;
		width: 120upx;
		color: #fff;
		position: relative;
		text-align: center;
		margin-bottom: 20upx;
	}
	.buticon image{
		height: 64upx;
		width: 64upx;
	}
	.buticon .mar10{
		margin-top: -20upx;
	}
	.martp10{
		margin-top: 10upx;

	}
	.give {
		width: 90upx;
		height: 90upx;
		background: #F44336;	
		border-radius: 50%;
		box-shadow: 0 0 22upx 0 rgb(252, 94, 20);
	 	 position: absolute; 
		left:15upx;
		top:15upx; 
		    font-size: 44upx;
    line-height: 90upx;
	}
	.givebegin {
		width: 60upx;
		height: 60upx;
		background: #F44336;	
		border-radius: 20%;
		box-shadow: 0 0 22upx 0 rgb(252, 94, 20);
	 	 position: absolute; 
		left:30upx;
		top:30upx; 
	}
	.x_f{
		/* border: 6upx solid #F44336; */
		width: 120upx;
		height: 120upx;
		background: #fff;
		border-radius: 50%;
		position: absolute;
		text-align: center;
		top:0;
		left: 0;
	  box-shadow: 0 0 28upx 0 rgb(251, 99, 24);
	}
	
	/* 產生動畫(向外擴散變大)的圓圈  */
	.pulse {
		width: 160upx;
		height: 160upx;
		position: absolute;
	    border: 12upx solid #F44336;
	    border-radius: 100%;
	    z-index: 1;
	    opacity: 0;
	    -webkit-animation: warn 2s ease-out;
	    animation: warn 2s ease-out;
	    -webkit-animation-iteration-count: infinite;
	    animation-iteration-count: infinite;
	    left: -28upx;
	    top: -28upx;
	}
		
	
	/**
	 * 動畫
	 */
	@keyframes warn {
	0% {
		transform: scale(0);
		opacity: 0.0;
	}
	25% {
		transform: scale(0);
		opacity: 0.1;
	}
	50% {
		transform: scale(0.1);
		opacity: 0.3;
	}
	75% {
		transform: scale(0.5);
		opacity: 0.5;
	}
	100% {
		transform: scale(1);
		opacity: 0.0;
	}
}
	
	 
</style>

14. 拉流演示代碼。

<template class='fullscreen'>
	<view class='fullscreen'>
		<view v-if="beCalling"  class="backols">
			<view class='becalling-text'>對方邀請你開始視頻聊天</view>
			<view class="butlist2">
				<view @click="rejectCallHandler" class="buticon2 martp10">
					<image src="../../static/img/netcall-reject.png"></image>	
				</view>
					<view @click="acceptCallHandler" class="buticon2 martp10">
						<image src="../../static/img/netcall-accept.png"></image>	
					</view>
				</view>
		</view>
		<view v-else class="butlist">
				<view @click="switchaudio" class="buticon martp10">
					<image src="../../static/img/netcall-call-voice.png"></image>	
				
				</view>
				<view @click="switchCamera" class="buticon martp10">
					<image src="../../static/img/netcall-revert-camera.png"></image>	
						
				</view>
				<view @click="close" class="buticon martp10">
					<image src="../../static/img/netcall-reject.png"></image>	
				</view>
			 
			</view>
	</view>
	
	
</template>

<script>
	export default {
		 data() {
			 return{
				beCalling: true,
				videourl:'',
				width:'',
				currentWebview:null,
				pushers:'',
				video :''
		  }
		},
		
		onLoad: function (options) {
				 this.getwebview()//獲取webview
		},
		onUnload() {
		
		},
		methods: {
				close(){
						 this.pusher.pause();//暫停推流
						this.pusher.close()//關閉推流控件
						uni.switchTab({
							url:''
						})
				},
				getwebview(){
					var pages = getCurrentPages();
					var page = pages[pages.length - 1];
					// #ifdef APP-PLUS
					var getcurrentWebview = page.$getAppWebview();
					console.log(this.pages)
					console.log(this.page)
					console.log(JSON.stringify(page.$getAppWebview()))
					this.currentWebview=getcurrentWebview;
					// #endif
					this.plusReady()//創建LivePusher對象
				},
				 
				plusReady(){				
				
					this.pushers =new plus.video.VideoPlayer('video',{
						// src:self.userlist[0].url,
						src:"rtmp://58.200.131.2:1935/livetv/hunantv", //這裏替換自己的拉流地址
						top:'0px',
						left:'0px',
						controls:false,
						width: '100%',
						height: uni.getSystemInfoSync().windowHeight-150 + 'px',
						position: 'static'		
					});				 
					this.currentWebview.append(this.pushers);
				 this.pushers.play()

				},
		 
		 
		 /**
			 * 切換攝像頭
			 */ 
			switchCamera() {
				this.pusher.switchCamera();
			},
			switchaudio() {
				console.log('點擊了');
			}
				
		}	
	}
	
</script>

<style>
	
	.backols{
	    background: rgba(0, 0, 0, 0.74);
    height: 100%;
    position: absolute;
    width: 100%;
	}
	uni-page{
		background:#000000;
	}
	.butlist{
		height: 140upx;
		position: absolute;
		bottom: 0;
		display: flex;
		width: 100%;
		justify-content: space-around;
	    padding-top: 20upx;
		border-top: 1px solid #fff;
	}
	.buticon{
		height: 120upx;
		width: 120upx;
		color: #fff;
		position: relative;
		text-align: center;
		margin-bottom: 20upx;
	}
	.buticon image{
		height: 90upx;
		width: 90upx;
	}
	.buticon .mar10{
		margin-top: -20upx;
	}
	.martp10{
		margin-top: 10upx;
	
	}
	.becalling-text{
		text-align: center;
		color: #FFFFFF;
		font-size: 28upx;
		padding: 60upx;
		margin-top: 40%;
	}
	.butlist2{
		height: 140upx;
		position: absolute;
		bottom: 5%;
		display: flex;
		width: 100%;
		justify-content: space-around;
	    padding-top: 20upx;
	 
	}
	.buticon2{
		height: 120upx;
		width: 120upx;
		color: #fff;
		position: relative;
		text-align: center;
		margin-bottom: 20upx;
	}
	.buticon2 image{
		height: 110upx;
		width: 110upx;
	}
 
 
	.container {
	  width: 100%;
	  height: 100%;
	}
	/* 被叫 */
	.becalling-wrapper {
	  position: relative;
	  width:100%;
	  height:800upx;
	  background-color:#777;
	  color:#fff;
	  font-size:40rpx;
	}
	.becalling-wrapper .becalling-text {
	  position: absolute;
	  top:400rpx;
	  left:50%;
	  margin-left:-220rpx;
	}
	.becalling-wrapper .becalling-button-group {
	  position: absolute;
	  width:100%;
	  box-sizing:border-box;
	  bottom: 100rpx;
	  padding: 0 40rpx;
	  display: flex;
	  flex-direction: row;
	  justify-content: space-between;
	}
	.becalling-button-group .button {
	  width:220rpx;
	  height:80rpx;
	  border-radius:10rpx;
	  justify-content:center;
	  display:flex;
	  align-items:center;
	  font-size:33rpx;
	  color:#000;
	}
	.becalling-button-group .reject-button {
	  background-color:#f00;
	}
	.becalling-button-group .accept-button {
	  background-color:rgb(26, 155, 252);
	}
	
	.calling-coverview {
	  width:100%;
	  height:100rpx;
	  background-color:#ccc;
	  color:#fff;
	  font-size:40rpx;
	  text-align:center;
	  line-height:100rpx;
	}
	/* 視頻容器 */
	.video-wrapper {
	  width: 100%;
	  height: 100%;
	  padding-bottom: 100rpx;
	  box-sizing: border-box;
	  position: relative;
	  background-color: #000;
	}
	.control-wrapper {
	  width: 100%;
	  box-sizing: border-box;
	  position: absolute;
	  bottom: 0;
	}
	.calling-voerview {
	  background-color:#ccc;
	  color:#fff;
	  height: 160rpx;
	  font-size: 40rpx;
	  text-align: center;
	  line-height: 160rpx;
	}
	.control-wrapper {
	  position: fixed;
	  bottom: 18px;
	  left:0;
	  display: flex;
	  width: 100%;
	  box-sizing: border-box;
	  flex-direction:row;
	  justify-content: space-between;
	  padding: 0 42rpx;
	  height: 200rpx;
	}
	.control-wrapper .item{
	  width: 92rpx;
	  height: 92rpx;
	  margin-top: 100rpx;
	}
	.netcall-time-text {
	  position:absolute;
	  bottom:160rpx;
	  width:100%;
	  height: 40rpx;
	  color:#fff;
	  font-size:40rpx;
	  text-align:center;
	  left:0;
	}
	
	
	.fullscreen{
		display: flex;
		background: #000000;
		height: 100%;
		width: 100%;
		position: absolute;
	}
	
</style>

15. uni-app模塊權限如下。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章