当前位置: 首页 > article >正文

如何设计开发RTSP直播播放器?

技术背景

我们在对接RTSP直播播放器相关技术诉求的时候,好多开发者,除了选用成熟的RTSP播放器外,还想知其然知其所以然,对RTSP播放器的整体开发有个基础的了解,方便方案之作和技术延伸。本文抛砖引玉,做个大概的介绍。

技术实现

技术难点

在探讨RTSP直播播放器技术实现之前,我们先来看,为什么RTSP播放器的开发看似简单,实则复杂,或者说做播放器容易,做个好的播放器,为什么就那么难?

协议复杂性
  1. 理解 RTSP 协议规范
    • RTSP 协议本身比较复杂,包含多种请求方法(如 OPTIONS、DESCRIBE、SETUP、PLAY、PAUSE、TEARDOWN 等)、状态码和头部字段。开发者需要深入理解这些规范,以便正确地与服务器进行交互。
    • 例如,在处理 DESCRIBE 请求时,需要解析服务器返回的媒体描述信息,其中可能包含复杂的 SDP(Session Description Protocol)格式,包括媒体类型、编码方式、帧率、分辨率等参数的描述。理解和解析这些信息需要对协议规范有深入的了解。
  2. 处理不同的协议变种和扩展
    • 在实际应用中,可能会遇到不同的 RTSP 服务器实现,它们可能会有一些协议变种或自定义的扩展。开发者需要能够处理这些差异,确保播放器能够与各种服务器兼容。
    • 例如,某些服务器可能会在 RTSP 响应中添加自定义的头部字段,或者对标准的请求方法有不同的处理方式。开发者需要能够识别这些差异,并进行相应的处理,以保证播放器的兼容性。
网络环境的不确定性
  1. 适应不同的网络条件
    • RTSP 播放器需要在各种网络环境下工作,包括不同的带宽、延迟、丢包率等。开发者需要考虑如何适应这些不同的网络条件,以确保视频的流畅播放。
    • 例如,在低带宽环境下,可能需要采用自适应比特率技术,根据网络状况动态调整视频的码率,以避免卡顿和缓冲。在高延迟网络中,需要优化播放控制算法,减少播放延迟,提高用户体验。
  2. 处理网络错误和异常情况
    • 网络环境中可能会出现各种错误和异常情况,如连接中断、服务器故障、丢包等。开发者需要能够处理这些情况,进行适当的错误恢复和重试机制,以保证播放器的稳定性。
    • 例如,当连接中断时,播放器需要能够自动尝试重新连接服务器,并在重新连接成功后继续播放。当出现丢包情况时,需要采用适当的错误隐藏技术,如帧间插值或重复上一帧,以减少视频的卡顿和花屏现象。
视频解码和播放的复杂性
  1. 支持多种视频编码格式
    • RTSP 流可以使用多种视频编码格式,如 H.264、H.265、MPEG-4 等。开发者需要选择合适的视频解码器,并确保播放器能够支持各种常见的编码格式。
    • 不同的编码格式具有不同的特点和复杂性,需要对其进行深入了解和处理。例如,H.265 编码具有更高的压缩率,但解码复杂度也更高。开发者需要选择高效的解码器,并进行优化,以确保在不同设备上的性能表现。
  2. 处理视频同步问题
    • 在播放视频时,需要确保音频和视频的同步播放。这涉及到处理视频和音频的时间戳、帧率、采样率等参数,以及进行适当的同步调整。
    • 视频和音频的同步是一个复杂的问题,需要考虑多种因素,如网络延迟、解码时间、播放设备的性能等。开发者需要采用适当的同步算法,确保音频和视频的同步播放,提高用户体验。
跨平台开发的挑战
  1. 适应不同的操作系统和设备
    • RTSP 播放器需要在不同的操作系统和设备上运行,如 Windows、Linux、Android、iOS 等。开发者需要考虑如何进行跨平台开发,确保播放器在各种平台上都能正常工作。
    • 不同的平台具有不同的开发环境、编程语言和多媒体框架,需要进行相应的适配和优化。例如,在 Android 平台上,可能需要使用 Java 或 Kotlin 进行开发,并利用 Android 的多媒体框架;在 iOS 平台上,可能需要使用 Objective-C 或 Swift 进行开发,并利用 iOS 的 AVFoundation 框架。
  2. 处理不同的硬件特性
    • 不同的设备具有不同的硬件特性,如处理器性能、内存大小、图形处理能力等。开发者需要考虑如何优化播放器的性能,以适应不同设备的硬件特性。
    • 例如,在性能较低的设备上,可能需要采用更高效的解码算法和播放控制策略,以减少资源占用和提高播放流畅性。在具有硬件加速功能的设备上,可以利用硬件加速来提高解码和播放性能。

技术选型

编程语言和平台
  • 选择适合的编程语言和开发平台。常见的选择包括 C++、Java、Python等编程语言,以及 Android、iOS、Windows、Linux 等操作系统平台。
  • 例如,在 Android平台上可以使用 Java 或 Kotlin 进行开发,利用 Android SDK 提供的多媒体框架和网络功能来实现 RTSP 播放器。,也可以通过jni接口封装,核心业务在底层,对上提供jni调用接口。
多媒体框架和库
  • 选择合适的多媒体框架和库来实现视频解码和播放功能。一些常用的多媒体框架和库包括 FFmpeg、GStreamer、VLC 等。
  • 这些框架和库提供了丰富的功能,如视频解码、音频解码、流媒体协议支持等,可以大大简化 RTSP 播放器的开发过程。例如,FFmpeg 是一个广泛使用的开源多媒体框架,支持众多的视频和音频格式以及流媒体协议,可以在多个平台上使用。
了解RTSP协议
  1. 协议结构和工作原理
    • 深入了解 RTSP 协议的结构和工作原理。RTSP 是一个应用层协议,用于控制实时流媒体的传输。它使用 TCP 或 UDP 作为传输层协议,通过发送请求和接收响应来实现对媒体流的控制。
    • RTSP 协议的主要功能包括媒体流的播放、暂停、快进、快退等操作,以及媒体流的描述、设置和传输控制等。了解 RTSP 协议的请求和响应格式、状态码、方法等内容,对于开发 RTSP 播放器至关重要。
  2. 协议交互过程
    • 熟悉 RTSP 协议的交互过程。当播放器连接到 RTSP 服务器时,首先发送 OPTIONS 请求以获取服务器支持的方法列表。然后,播放器发送 DESCRIBE 请求获取媒体流的描述信息,包括媒体格式、编码方式、帧率等。
    • 根据媒体流的描述信息,播放器选择合适的解码器进行视频和音频解码。接下来,播放器发送 SETUP 请求建立媒体流的传输连接,并发送 PLAY 请求开始播放媒体流。在播放过程中,播放器可以发送 PAUSE、TEARDOWN 等请求来控制媒体流的播放状态。

实现播放器功能

网络连接和数据接收
  • 实现与 RTSP 服务器的网络连接和数据接收功能。使用所选编程语言的网络编程库,建立与 RTSP 服务器的 TCP 或 UDP 连接,并接收服务器发送的媒体流数据。
  • 在接收数据时,需要处理网络错误、丢包等情况,确保数据的完整性和准确性。可以使用缓冲区来存储接收到的数据,以便后续的解码和播放操作。
视频解码和播放
  • 选择合适的视频解码器对接收的媒体流数据进行解码,并将解码后的视频帧显示在屏幕上。根据所选的多媒体框架和库,配置解码器参数,如视频格式、分辨率、帧率等。
  • 对于视频播放,可以使用图形库或多媒体框架提供的显示功能,将解码后的视频帧绘制在窗口或视图中。同时,需要处理视频的同步问题,确保音频和视频的同步播放。
音频解码和播放
  • 对接收的媒体流数据中的音频部分进行解码,并通过音频设备播放出来。选择合适的音频解码器,配置解码器参数,如音频格式、采样率、声道数等。
  • 使用音频输出库或多媒体框架提供的音频播放功能,将解码后的音频数据发送到音频设备进行播放。同样,需要处理音频的同步问题,确保音频和视频的同步播放。
播放控制和用户界面
  • 实现播放控制功能,如播放、暂停、快进、快退等操作。通过发送相应的 RTSP 请求来控制媒体流的播放状态,并在用户界面上提供相应的控制按钮。
  • 设计用户界面,包括视频显示区域、播放控制按钮、进度条等。使用图形用户界面库或开发平台提供的界面设计工具,创建直观、易用的用户界面。

SmartPlayer设计实现

以大牛直播SDK的SmartPlayer RTSP直播播放模块为例,我们来看看,如何实现低延迟的RTSP播放器。大牛直播SDK自2015年发布RTSP、RTMP直播播放模块,迭代从未停止,SmartPlayer功能强大、性能强劲、高稳定、超低延迟、超低资源占用。无需赘述,全自研内核,行业内一致认可的跨平台RTSP、RTMP直播播放器。先说功能设计,如不单独说明,Windows、Linux(x86_64|aarch64架构)、Android、iOS全平台支持。

  •  [多实例播放]支持多实例播放;
  •  [事件回调]支持网络状态、buffer状态等回调;
  •  [视频格式]支持H.265、H.264,此外,还支持RTSP MJPEG播放;
  •  [音频格式]支持AAC/PCMA/PCMU;
  •  [H.264/H.265软解码]支持H.264/H.265软解;
  •  [H.264硬解码]Windows/Android/iOS支持特定机型H.264硬解;
  •  [H.265硬解]Windows/Android/iOS支持特定机型H.265硬解;
  •  [H.264/H.265硬解码]Android支持设置Surface模式硬解和普通模式硬解码;
  •  [RTSP模式设置]支持RTSP TCP/UDP模式设置;
  •  [RTSP TCP/UDP自动切换]支持RTSP TCP、UDP模式自动切换;
  •  [RTSP超时设置]支持RTSP超时时间设置,单位:秒;
  •  [RTSP 401认证处理]支持上报RTSP 401事件,如URL携带鉴权信息,会自动处理;
  •  [缓冲时间设置]支持buffer time设置;
  •  [首屏秒开]支持首屏秒开模式;
  •  [复杂网络处理]支持断网重连等各种网络环境自动适配;
  •  [快速切换URL]支持播放过程中,快速切换其他URL,内容切换更快;
  •  [音视频多种render机制]Android平台,视频:surfaceview/OpenGL ES,音频:AudioTrack/OpenSL ES;
  •  [实时静音]支持播放过程中,实时静音/取消静音;
  •  [实时音量调节]支持播放过程中实时调节音量;
  •  [实时快照]支持播放过程中截取当前播放画面;
  •  [只播关键帧]Windows平台支持实时设置是否只播放关键帧;
  •  [渲染角度]支持0°,90°,180°和270°四个视频画面渲染角度设置;
  •  [渲染镜像]支持水平反转、垂直反转模式设置;
  •  [等比例缩放]支持图像等比例缩放绘制(Android设置surface模式硬解模式不支持);
  •  [实时下载速度更新]支持当前下载速度实时回调(支持设置回调时间间隔);
  •  [解码前视频数据回调]支持H.264/H.265数据回调;
  •  [解码后视频数据回调]支持解码后YUV/RGB数据回调;
  •  [解码前音频数据回调]支持AAC/PCMA/PCMU数据回调;
  •  [音视频自适应]支持播放过程中,音视频信息改变后自适应;
  •  [扩展录像功能]完美支持和录像SDK组合使用。
RTSP播放器设计要点

1. 低延迟:大多数RTSP的播放都面向直播场景,所以,如果延迟过大,严重影响体验,所以,低延迟是衡量一个好的RTSP播放器非常重要的指标,目前大牛直播SDK的RTSP直播播放延迟比开源播放器更优异,而且长时间运行下,不会造成延迟累积;

2. 音视频同步处理有些播放器为了追求低延迟,甚至不做音视频同步,拿到audio video直接播放,导致a/v不同步,还有就是时间戳乱跳等各种问题,大牛直播SDK提供的播放器,具备好的时间戳同步和异常时间戳矫正机制;

3. 支持多实例:大牛直播SDK提供的播放器支持同时播放多路音视频数据,比如4-8-9窗口,大多开源播放器对多实例支持不太友好;

4. 支持buffer time设置:在一些有网络抖动的场景,播放器需要支持buffer time设置,一般来说,以毫秒计,开源播放器对此支持不够友好;

5. TCP/UDP模式设定自动切换:考虑到好多服务器仅支持TCP或UDP模式,一个好的RTSP播放器需要支持TCP/UDP模式设置,如链接不支持TCP或UDP,大牛直播SDK可自动切换,,开源播放器不具备自动切换TCP/UDP能力;

6. 实时静音:比如,多窗口播放RTSP流,如果每个audio都播放出来,体验非常不好,所以实时静音功能非常必要,开源播放器不具备实时静音功能;

7. 视频view旋转:好多摄像头由于安装限制,导致图像倒置,所以一个好的RTSP播放器应该支持如视频view实时旋转(0° 90° 180° 270°)、水平反转、垂直反转,开源播放器不具备此功能;

8. 支持解码后audio/video数据输出:大牛直播SDK接触到好多开发者,希望能在播放的同时,获取到YUV或RGB数据,进行人脸匹配等算法分析,开源播放器不具备此功能;

9. 实时快照:感兴趣或重要的画面,实时截取下来非常必要,一般播放器不具备快照能力,开源播放器不具备此功能;

10. 网络抖动处理(如断网重连):稳定的网络处理机制、支持如断网重连等,开源播放器对网络异常处理支持较差;

11. 长期运行稳定性:不同于市面上的开源播放器,大牛直播SDK提供的Windows平台RTSP直播播放SDK适用于数天长时间运行,开源播放器对长时间运行稳定性支持较差;

12. log信息记录:整体流程机制记录到LOG文件,确保出问题时,有据可依,开源播放器几无log记录。

13. 实时下载速度反馈:大牛直播SDK提供音视频流实时下载回调,并可设置回调时间间隔,确保实时下载速度反馈,以此来监听网络状态,开源播放器不具备此能力;

14. 异常状态处理Event状态回调如播放的过程中,断网、网络抖动、等各种场景,大牛直播SDK提供的播放器可实时回调相关状态,确保上层模块感知处理,开源播放器对此支持不好;

15. 关键帧/全帧播放实时切换:特别是播放多路画面的时候,如果路数过多,全部解码、绘制,系统资源占用会加大,如果能灵活的处理,可以随时只播放关键帧,全帧播放切换,对系统性能要求大幅降低。

Android平台RTSP播放示例

下面以Android平台多实例RTSP播放为例,探讨下接口设计和调用说明。

我们针对的功能展示,主要是播放和录像这块,先说播放:

/*
 * SmartPlayer.java
 * Author: https://daniusdk.com
 * WeChat: xinsheng120
 * Created by DaniuLive on 2015/09/26.
 */
class ButtonPlayback1Listener implements View.OnClickListener {
	public void onClick(View v) {
		if (stream_player_1_.is_playing()) {
			Log.i(TAG, "Stop player1..");

			boolean iRet = stream_player_1_.StopPlayer();

			if (!iRet) {
				Log.e(TAG, "Call StopPlayer failed..");
				return;
			}

			stream_player_1_.try_release();
			btn_playback1.setText("开始播放1");
			SetViewVisibility(surface_view_1_);
		} else {
			Log.i(TAG, "Start playback stream1++");

			int play_buffer = 0;
			int is_using_tcp = 0;
			if(!stream_player_1_.OpenPlayerHandle(playback_url_1_, play_buffer, is_using_tcp))
				return;

			stream_player_1_.SetView(surface_view_1_);

			boolean is_mute = false;
			boolean iPlaybackRet = stream_player_1_.StartPlayer(isHardwareDecoder, is_enable_hardware_render_mode, is_mute);
			if (!iPlaybackRet) {
				Log.e(TAG, "Call StartPlayer failed..");
				return;
			}

			btn_playback1.setText("停止播放1");
		}
	}
}

对应的OpenPlayerHandle()实现如下:

/*
 * LibPlayerWrapper.java.java
 * Author: https://daniusdk.com
 */
public boolean OpenPlayerHandle(String playback_url, int play_buffer, int is_using_tcp) {

	if (check_native_handle())
		return true;

	if(!isValidRtspOrRtmpUrl(playback_url))
		return false;

	long handle = lib_player_.SmartPlayerOpen(application_context());
	if (0==handle) {
		Log.e(TAG, "sdk open failed!");
		return false;
	}

	lib_player_.SetSmartPlayerEventCallbackV2(handle, new EventHandleV2());

	lib_player_.SmartPlayerSetBuffer(handle, play_buffer);

	// set report download speed(默认2秒一次回调 用户可自行调整report间隔)
	lib_player_.SmartPlayerSetReportDownloadSpeed(handle, 1, 4);

	boolean isFastStartup = true;
	lib_player_.SmartPlayerSetFastStartup(handle, isFastStartup ? 1 : 0);

	//设置RTSP超时时间
	int rtsp_timeout = 10;
	lib_player_.SmartPlayerSetRTSPTimeout(handle, rtsp_timeout);

	//设置RTSP TCP/UDP模式自动切换
	int is_auto_switch_tcp_udp = 1;
	lib_player_.SmartPlayerSetRTSPAutoSwitchTcpUdp(handle, is_auto_switch_tcp_udp);

	lib_player_.SmartPlayerSaveImageFlag(handle, 1);

	// It only used when playback RTSP stream..
	lib_player_.SmartPlayerSetRTSPTcpMode(handle, is_using_tcp);

	lib_player_.DisableEnhancedRTMP(handle, 0);

	lib_player_.SmartPlayerSetUrl(handle, playback_url);

	set(handle);

	return true;
}

对应的开始播放、停止播放设计:

/*
 * LibPlayerWrapper.java
 * Author: https://daniusdk.com
 */
public boolean StartPlayer(boolean is_hardware_decoder, boolean is_enable_hardware_render_mode, boolean is_mute) {
	if (is_playing()) {
		Log.e(TAG, "already playing, native_handle:" + get());
		return false;
	}

	SetPlayerParam(is_hardware_decoder, is_enable_hardware_render_mode, is_mute);

	int ret = lib_player_.SmartPlayerStartPlay(get());
	if (ret != OK) {
		Log.e(TAG, "call StartPlay failed, native_handle:" + get() + ", ret:" + ret);
		return false;
	}

	write_lock_.lock();
	try {
		this.is_playing_ = true;
	} finally {
		write_lock_.unlock();
	}

	Log.i(TAG, "call StartPlayer OK, native_handle:" + get());
	return true;
}

public boolean StopPlayer() {
	if (!check_native_handle())
		return false;

	if (!is_playing()) {
		Log.w(TAG, "it's not playing, native_handle:" + get());
		return false;
	}

	boolean is_need_call = false;
	write_lock_.lock();
	try {
		if (this.is_playing_) {
			this.is_playing_ = false;
			is_need_call = true;
		}
	} finally {
		write_lock_.unlock();
	}

	if (is_need_call)
		lib_player_.SmartPlayerStopPlay(get());

	return true;
}

录像设计:

/*
 * SmartPlayer.java
 * Author: https://daniusdk.com
 */
class ButtonRecorder1Listener implements View.OnClickListener {
	public void onClick(View v) {
		if (stream_player_1_.is_recording()) {
			Log.i(TAG, "Stop recorder1..");

			boolean iRet = stream_player_1_.StopRecorder();

			if (!iRet) {
				Log.e(TAG, "Call StopRecorder failed..");
				return;
			}

			stream_player_1_.try_release();
			btn_recorder1.setText("开始录像1");
		} else {
			Log.i(TAG, "Start recorder stream1++");

			int play_buffer = 0;
			int is_using_tcp = 0;

			if(!stream_player_1_.OpenPlayerHandle(playback_url_1_, play_buffer, is_using_tcp))
				return;

			stream_player_1_.ConfigRecorderParam(recDir, 400, 1, 1, 1);

			boolean iRecRet = stream_player_1_.StartRecorder();
			if (!iRecRet) {
				Log.e(TAG, "Call StartRecorder failed..");
				return;
			}

			btn_recorder1.setText("停止录像1");
		}
	}
}

录像参数配置选项:

/*
 * LibPlayerWrapper.java
 * Author: https://daniusdk.com
 */
public boolean ConfigRecorderParam(String rec_dir, int file_max_size, int is_transcode_aac,
								   int is_record_video, int is_record_audio) {

	if(!check_native_handle())
		return false;

	if (null == rec_dir || rec_dir.isEmpty())
		return false;

	int ret = lib_player_.SmartPlayerCreateFileDirectory(rec_dir);
	if (ret != 0) {
		Log.e(TAG, "Create record dir failed, path:" + rec_dir);
		return false;
	}

	if (lib_player_.SmartPlayerSetRecorderDirectory(get(), rec_dir) != 0) {
		Log.e(TAG, "Set record dir failed , path:" + rec_dir);
		return false;
	}

	if (lib_player_.SmartPlayerSetRecorderFileMaxSize(get(),file_max_size) != 0) {
		Log.e(TAG, "SmartPlayerSetRecorderFileMaxSize failed.");
		return false;
	}

	lib_player_.SmartPlayerSetRecorderAudioTranscodeAAC(get(), is_transcode_aac);

	// 更细粒度控制录像的, 一般情况无需调用
	lib_player_.SmartPlayerSetRecorderVideo(get(), is_record_video);
	lib_player_.SmartPlayerSetRecorderAudio(get(), is_record_audio);
	return true;
}

开始录像、结束录像:

/*
 * LibPlayerWrapper.java
 * Author: https://daniusdk.com
 */
public boolean StartRecorder() {

	if (is_recording()) {
		Log.e(TAG, "already recording, native_handle:" + get());
		return false;
	}

	int ret = lib_player_.SmartPlayerStartRecorder(get());
	if (ret != OK) {
		Log.e(TAG, "call SmartPlayerStartRecorder failed, native_handle:" + get() + ", ret:" + ret);
		return false;
	}

	write_lock_.lock();
	try {
		this.is_recording_ = true;
	} finally {
		write_lock_.unlock();
	}

	Log.i(TAG, "call SmartPlayerStartRecorder OK, native_handle:" + get());
	return true;
}

public boolean StopRecorder() {
	if (!check_native_handle())
		return false;

	if (!is_recording()) {
		Log.w(TAG, "it's not recording, native_handle:" + get());
		return false;
	}

	boolean is_need_call = false;
	write_lock_.lock();
	try {
		if (this.is_recording_) {
			this.is_recording_ = false;
			is_need_call = true;
		}
	} finally {
		write_lock_.unlock();
	}

	if (is_need_call)
		lib_player_.SmartPlayerStopRecorder(get());

	return true;
}

总结

做RTSP播放器容易,做个可以稳定用于实际场景的低延迟RTSP播放器,真的非常困难,首先,RTSP协议本身的复杂度,如果不涉及底层协议栈,只是开源的项目编译调试小修小改,遇到问题,很难处理。还有就是网络环境的不确定性,视频解码和播放的复杂性,视频同步问题的复杂性及考虑因素。最后在跨平台开发的挑战,不同操作系统和设备以及处理不同硬件特性,都需要考虑。以上抛砖引玉,感兴趣的开发者,可以单独跟我沟通探讨。


http://www.kler.cn/a/354970.html

相关文章:

  • 重温设计模式--单例模式
  • 下载运行Vue开源项目vue-pure-admin
  • 【论文复现】农作物病害分类(Web端实现)
  • CASA(Carnegie-Ames-Stanford Approach) 模型原理及实践
  • sfnt-pingpong -测试网络性能和延迟的工具
  • 鸿蒙Next之包体积极限优化
  • 基于直播美颜SDK的实时美颜平台开发指南
  • 如何启动hive
  • 软件分享|Notepad-- 文本编辑器
  • 基于逻辑回归实现乳腺癌预测
  • Rocky linux 修改ip地址, rocky服务器修改静态地址, rocky虚拟机修改ip
  • ai字幕用什么软件制作?6款视频加字幕工具分享!
  • 29.第二阶段x86游戏实战2-遍历周围-花指令与二叉树数据结构(有如何阅读vm代码混淆代码)
  • 【微信小程序_7_WXML 模板语法 - 按钮事件和输入框事件绑定】
  • rk3568 , rk3588 , rknpu2 sdk , rknn-toolkit2, rknn-toolkit2-lite 的了解
  • AI 工具大比拼:ChatGPT 与豆包的魅力与未来展望
  • unity 屏幕波动反馈打击效果(附资源下载)
  • 基于matlab处理语音信号
  • C++新手入门指南:从基础概念到实践之路
  • Kafka之消费者组与消费者
  • YOLOv11改进-模块-引入矩形自校准模块RCM有利于复杂场景(小目标、遮挡等)
  • Python解力扣算法题4(基础)
  • linux中通过一个命令启动任何java的jar包
  • Spring Boot + Vue 前后端分离项目总结:解决 CORS 和 404 问题
  • zipkin启动脚本并指定mysql数据存储
  • 探索桂林:使用SpringBoot构建的旅游平台