超越播放器:使用 libVLC SDK 构建你自己的跨平台媒体应用
超越播放器:使用 libVLC SDK 构建你自己的跨平台媒体应用
引言
在多媒体处理领域,VLC 媒体播放器 几乎无人不知,但您是否了解其背后强大的核心引擎 —— VLC(Video Lan Client)库? 作为一名开发者,直接使用 VLC 播放器只是“消费”,而调用 VLC 库 则是“创造”。它允许你将强大的多媒体播放、流媒体处理、转码等功能无缝集成到自己的应用程序中。本文将带你深入探索 VLC 库的世界。
1. 什么是 VLC 库?
VLC 库,通常指 libVLC 或 VLC 引擎,是开源媒体播放器 VLC 的核心部分。它是一个功能完整、高度可移植的多媒体框架和 SDK,封装了 VLC 的全部播放与控制能力。VLC库的核心理念是一切都是流的播放,因此基于此理念,服务器就是一个向客户端进行播放的播放器;客户端就是一个将数据播放给用户看的播放器。
核心特性
- 开源免费:基于
GPL/LGPL许可证,可免费用于开源及部分商业项目。 - 跨平台:支持
Windows、MacOS、Linux、Android、iOS等主流平台。 - 格式支持广泛
- 支持的访问形式:文件、DVD/VCD/CD、http、ftp、mms、TCP、UDP、RTP、IP组播、IPv6、rtsp
- 编码格式:MPEG*、DIVX、WMV、MOV、3GP、FLV、H.263、H.264、FLAC
- 视频字幕:DVD、DVB、Text、Vobsub
- 视频输出:DirectX、X11、XVideo、SDL、FrameBuffer、ASCII
- 控制界面:WxWidgets、QT、Web、Telent、Command line、MFC
- 浏览器插件:ActiveX、Mozilla(firefox)
- 无需外部编解码器:自带众多解码器(如 FFmpeg),开箱即用。
2. 核心架构与工作原理
VLC 采用模块化设计,其核心是一个轻量的 libvlc库,通过插件动态加载所需功能模块。
- 输入模块:负责读取数据(文件、网络流、设备)。
- 解码器模块:负责解码音频、视频、字幕编码。
- 滤镜模块:提供音视频处理(如缩放、色彩转换)。
- 输出模块:将处理后的数据送至音频设备、显卡或文件。
3. 主要功能与应用场景
3.1 多媒体播放与控制
- 基础播放:播放本地文件或网络流。
- 高级控制:播放、暂停、跳转、速率控制、音量调节、全屏等。
- 轨道管理:切换音频轨、字幕轨、视频角度。
3.2 流媒体处理
- 流媒体播放:作为客户端播放 RTSP、RTMP、HLS 等流,将其转化视频进行播放。
- 流媒体服务:作为服务器,将媒体文件或设备输入转换为 RTSP、HTTP 等流并推送给客户端。
- 转码与流转发:实时将一种格式的流转换为另一种格式并转发。
3.3 媒体处理与转码
- 格式转换:将媒体文件从一种格式转换为另一种(如 MP4 转 MP3,MKV 转 AVI)。
- 元数据提取:获取媒体文件的时长、编码信息、分辨率、帧率等。
典型应用场景
- 自定义媒体播放器:为你的应用嵌入一个功能强大的播放器,实现各种音视频的播放。
- 网络视频监控:连接 IP 摄像机(RTSP)并显示。
- 在线教育/视频会议:处理音视频的播放、录制与流转发。
- 广告机/数字标牌:轮播多媒体内容。
- 媒体服务器:搭建简单的个人流媒体服务器。
4. 核心 API 简介 (libvlc)和使用流程
libvlc提供 C 语言 API,其他语言(C++, Python, Java, C#等)的绑定通常基于此进行封装,借助这些libvlc的API可以快速实现流媒体客户端或服务器。
4.1 使用流程
- 一般先创建
VLC实例、创建要播放的视频媒体、再创建视频媒体的播放器。 - 解析媒体并获取媒体信息,然后设置播放器播放视频的音量、窗口、尺寸等属性,注册播放器事件的回调函数。
- 开始播放、暂停、停止等播放控制。
- 最后释放创建的播放器、视频媒体、
VLC实例。
4.2 libvlc核心API介绍
4.2.1 实例与核心对象创建和释放
-
创建/释放 VLC 实例
// 创建VLC实例 // 成功时返回指向libvlc_instance_t的指针,失败时返回 NULL LIBVLC_API libvlc_instance_t * libvlc_new(int argc, const char *const *argv) // 释放VLC实例 void libvlc_media_player_set_position(libvlc_instance_t * p_instance)libvlc_instance_t:VLC 库的全局实例,是使用任何功能的起点。argc:命令行参数数量,不包括最后表示结束的NULL指针argv:命令行参数数组指针,最后参数以NULL结尾const char *args[] = { "--quiet", // 静默模式,不打印控制台输出 "--no-video-title-show", // 不显示视频标题 "--no-audio", // 禁用音频输出 "--intf=dummy", // 使用哑接口,不创建任何窗口 "--no-stats", // 禁用统计信息 "--avcodec-hw=dxva2", // 启用硬件解码(DXVA2) "--network-caching=1000", // 网络缓存1秒 "--file-caching=500", // 文件缓存500毫秒 "--verbose=2", // 设置日志级别为警告 NULL // 标识命令行参数结束 }; int arg_count = sizeof(args)/sizeof(args[0]) - 1; // NULL不表示一个参数 libvlc_instance_t *inst = libvlc_new(arg_count, args); -
创建/释放指定视频数据媒体
// 1.创建指定文件路径下的视频数据的媒体 // 成功时返回指向libvlc_media_t的指针,失败时返回NULL LIBVLC_API libvlc_media_t * libvlc_media_new_path(libvlc_instance_t *p_instance, const char *psz_path) // 2.创建指定URL资源路径下的媒体 // 成功时返回指向libvlc_media_t的指针,失败时返回NULL LIBVLC_API libvlc_media_t * libvlc_media_new_location(libvlc_instance_t *p_instance, const char *psz_mrl) // 释放媒体 void libvlc_media_release(libvlc_media_t * p_md)libvlc_media_t:代表一个可播放的媒体项(文件路径、URL 等)。p_instance:VLC 实例指针psz_path:需要使用UTF-8编码格式的文件路径字符串,在Windows环境下的文件路径中有中文时要转化为UTF-8编码格式psz_mrl:媒体资源定位符URL,包括http://、rtsp://、file:///、screen://等// 1. 从指定视频文件路径创建媒体 // Windows libvlc_media_t *media = libvlc_media_new_path(inst, "C:\\Videos\\sample.mp4"); libvlc_media_t *media = libvlc_media_new_path(inst, "C:/Videos/sample.mp4"); // Linux/macOS libvlc_media_t *media = libvlc_media_new_path(inst, "/home/user/videos/sample.mp4"); // 中文路径处理 #ifdef _WIN32 // Windows 需要 UTF-8 编码 const char *utf8_path = "C:\\视频\\测试.mp4"; #else // Linux/macOS 本身是 UTF-8 编码 const char *utf8_path = "/home/user/视频/测试.mp4"; #endif libvlc_media_t *media = libvlc_media_new_path(inst, utf8_path); // 2. 从媒体资源定位符URL创建媒体 // HTTP/HTTPS 流 libvlc_media_t *media = libvlc_media_new_location(inst, "http://example.com/video.mp4"); // RTSP 流 (IP摄像头) libvlc_media_t *media = libvlc_media_new_location(inst, "rtsp://admin:password@192.168.1.100:554/stream1"); // RTMP 流 libvlc_media_t *media = libvlc_media_new_location(inst, "rtmp://live.example.com/app/stream"); // HLS 流 libvlc_media_t *media = libvlc_media_new_location(inst, "http://example.com/stream.m3u8"); // UDP 组播 libvlc_media_t *media = libvlc_media_new_location(inst, "udp://@239.255.12.42:1234"); // 屏幕捕获 (各平台不同) #ifdef _WIN32 libvlc_media_t *media = libvlc_media_new_location(inst, "screen://"); #elif defined(__APPLE__) libvlc_media_t *media = libvlc_media_new_location(inst, "screen://"); #elif defined(__linux__) libvlc_media_t *media = libvlc_media_new_location(inst, "screen://"); #endif // 摄像头捕获 #ifdef _WIN32 libvlc_media_t *media = libvlc_media_new_location(inst, "dshow://"); #elif defined(__APPLE__) libvlc_media_t *media = libvlc_media_new_location(inst, "avfoundation://"); #endif特别的还允许附带认证方式创建媒体
带认证的URL: // 基本认证 libvlc_media_t *media = libvlc_media_new_location(inst, "http://username:password@example.com/video.mp4"); // 或通过选项设置 libvlc_media_t *media = libvlc_media_new_location(inst, "rtsp://example.com/stream"); libvlc_media_add_option(media, ":rtsp-user=username"); libvlc_media_add_option(media, ":rtsp-pwd=password"); -
创建/释放指定媒体的播放器
// 1. 从指定媒体创建播放器 // 创建成功返回libvlc_media_player_t播放器,创建失败返回NULL LIBVLC_API libvlc_media_player_t * libvlc_media_player_new_from_media(libvlc_media_t *p_md) // 2. 从VLC示例创建播放器,然后绑定指定媒体 // 创建成功返回libvlc_media_player_t播放器,创建失败返回NULL LIBVLC_API libvlc_media_player_t * libvlc_media_player_new(libvlc_instance_t *p_instance) // 播放器绑定到指定媒体 LIBVLC_API void libvlc_media_player_set_media(libvlc_media_player_t *p_mi, libvlc_media_t *p_md) // 是否指定媒体的播放器 void libvlc_media_player_release(libvlc_media_player_t * p_mi)p_mi: 创建的播放器p_md: 被绑定的指定媒体,可以设置为NULL表示清空该播放器绑定的媒体 一般开始时采用第一种方式创建某个媒体的播放器,如果创建完指定媒体的播放器后不在需要使用该媒体时,可以直接释放掉该媒体,播放器内部会保留该媒体。
在第二种方式设置播放器指定的新媒体时,需要先暂停原媒体的播放,并获取播放器内部的原媒体进行释放,然后再绑定指定的新媒体。
// 1. 设置新媒体前需要停止原媒体的播放 if (libvlc_media_player_is_playing(player)) libvlc_media_player_stop(player); // 2. 获取当前媒体,并进行释放 libvlc_media_t *old_media = libvlc_media_player_get_media(player); if (old_media) libvlc_media_release(old_media); // 3. 设置新媒体 libvlc_media_t *new_media = libvlc_media_new_path(inst, "new_video.mp4"); libvlc_media_player_set_media(player, new_media);
4.2.2 媒体源属性设置与信息解析、音视频输出控制
-
在播放前设置添加媒体选项
// 在播放前设置指定媒体的选项 LIBVLC_API void libvlc_media_add_option(libvlc_media_t *p_md, const char *psz_options)p_md:媒体对象指针psz_options:选项字符串,以冒号开头,包括以下常见选项:// 网络相关选项 libvlc_media_add_option(p_md, ":network-caching=1000"); // 网络缓存1秒 libvlc_media_add_option(p_md, ":rtsp-tcp"); // RTSP使用TCP传输 libvlc_media_add_option(p_md, ":rtsp-timeout=5000"); // RTSP超时5秒 libvlc_media_add_option(p_md, ":http-user-agent=MyPlayer"); // 自定义User-Agent // 解码相关选项 libvlc_media_add_option(p_md, ":avcodec-hw=dxva2"); // 硬件解码 libvlc_media_add_option(p_md, ":avcodec-threads=4"); // 解码线程数 libvlc_media_add_option(p_md, ":avcodec-skiploopfilter=1"); // 跳过去块滤波 // 音频相关选项 libvlc_media_add_option(p_md, ":audio-language=chi"); // 首选中文音频 libvlc_media_add_option(p_md, ":audio-track=1"); // 指定音频轨道 libvlc_media_add_option(p_md, ":no-audio"); // 禁用音频 // 视频相关选项 libvlc_media_add_option(p_md, ":video-language=chi"); // 首选中文字幕 libvlc_media_add_option(p_md, ":no-video"); // 禁用视频 libvlc_media_add_option(p_md, ":video-filter=deinterlace"); // 视频滤镜 // 字幕相关选项 libvlc_media_add_option(p_md, ":sub-file=subtitle.srt"); // 外挂字幕 libvlc_media_add_option(p_md, ":sub-language=chi"); // 首选中文字幕 libvlc_media_add_option(p_md, ":sub-track=1"); // 指定字幕轨道 // 流输出选项 libvlc_media_add_option(p_md, ":sout=#transcode{vcodec=h264,vb=800}:duplicate{dst=std{access=file,mux=mp4,dst=output.mp4}}"); -
解析获取媒体信息
// 解析指定媒体信息,返回0表示异步解析已启动,返回-1表示解析失败 LIBVLC_API int libvlc_media_parse_with_options(libvlc_media_t *p_md, unsigned parse_flag, int timeout)p_md:媒体对象指针parse_flag:解析标志,控制解析是否采用阻塞方式(VLC_MEDIA_PARSE_BLOCKING)或异步方式(VLC_MEDIA_PARSE_UNBLOCKING),异步方式一般需要结合事件监听实现timeout:超时时间(毫秒),-1 表示无限等待注意如果不先等待解析获取媒体信息完成,直接获取媒体信息会直接获取错误数据。
// 1. 同步解析 - 阻塞当前线程直到解析完成 libvlc_media_parse_with_options(media, VLC_MEDIA_PARSE_BLOCKING, // 关键标志:阻塞模式 -1 // 超时时间(毫秒),-1表示无限等待 ); // 执行到这里时,解析已经完成 libvlc_time_t duration = libvlc_media_get_duration(media); // 可以立即获取有效值 // 2. 异步解析 - 异步执行解析,通过注册事件监听的回调函数实现 // 首先设置事件监听 libvlc_event_manager_t *em = libvlc_media_event_manager(media); libvlc_event_attach(em, libvlc_MediaParsedChanged, parse_callback, user_data); // 异步解析 - 立即返回,不阻塞 libvlc_media_parse_with_options(media, VLC_MEDIA_PARSE_UNBLOCKING, // 关键标志:非阻塞模式 -1 ); // 函数立即返回,继续执行其他代码 // 解析完成后会触发回调 -
获取媒体时长
// 从媒体中获取媒体时长,成功返回媒体时长单位为毫秒,失败返回-1 LIBVLC_API libvlc_time_t libvlc_media_get_duration(libvlc_media_t *p_md) // 从播放器中获取播放器时长,成功返回媒体时长单位为毫秒,失败返回-1 LIBVLC_API libvlc_time_t libvlc_media_player_get_length(libvlc_media_player_t *p_mi); -
播放器音量设置
音量值 (0-100, 0=静音, 100=最大)
// 设置播放器当前音量,成功返回0,失败返回-1 LIBVLC_API int libvlc_audio_set_volume(libvlc_media_player_t *p_mi, int i_volume) // 获取播放器当前音量 LIBVLC_API int libvlc_audio_get_volume(libvlc_media_player_t *p_mi); // 设置静音,status为0表示取消静音,为1表示取消静音,成功返回0,失败返回-1 LIBVLC_API int libvlc_audio_set_mute(libvlc_media_player_t *p_mi, int status); // 获取静音状态 LIBVLC_API int libvlc_audio_get_mute(libvlc_media_player_t *p_mi); // 切换静音状态,在静音和恢复音量状态间进行切换 LIBVLC_API void libvlc_audio_toggle_mute(libvlc_media_player_t *p_mi); -
设置窗口播放音视频
在指定窗口播放音视频
// Linux环境下使用X11的窗口播放视频 LIBVLC_API void libvlc_media_player_set_xwindow(libvlc_media_player_t *p_mi, uint32_t drawable); -
设置或获取视频尺寸
// 获取视频尺寸,num指定轨道数,px指定原视频宽度地址,py指定原视频高度地址 // 成功返回0,失败返回-1 LIBVLC_API int libvlc_video_get_size( libvlc_media_player_t *p_mi, unsigned num, unsigned *px, unsigned *py ); // 设置宽高比,paz_aspect指定宽高比格式字符串:"宽度:高度"或特殊值("default"、"fill"、NULL) LIBVLC_API void libvlc_video_set_aspect_ratio( libvlc_media_player_t *p_mi, const char *psz_aspect ); // 获取宽高比的格式字符串 LIBVLC_API char * libvlc_video_get_aspect_ratio(libvlc_media_player_t *p_mi); // 设置裁剪几何,psz_geometry表示裁剪比例字符串:“宽:高"、”宽度x高度+左偏移+上偏移“、"宽度x高度"、特殊值(NULL、"defult") LIBVLC_API void libvlc_video_set_crop_geometry( libvlc_media_player_t *p_mi, const char *psz_geometry );
4.2.3 播放控制和状态管理
-
开始播放
从当前位置开始播放媒体
// 开始播放器的播放,成功返回0,失败返回-1 LIBVLC_API int libvlc_media_player_play(libvlc_media_player_t *p_mi) -
暂停播放
暂退当前媒体播放,并保留媒体播放位置,可以再次使用从当前位置继续播放
// 暂停播放器的播放 LIBVLC_API void libvlc_media_player_pause(libvlc_media_player_t *p_mi) -
停止播放
停止当前媒体播放,再次播放时会从头开始播放
// 停止结束播放器的播放 LIBVLC_API void libvlc_media_player_stop(libvlc_media_player_t *p_mi) -
设置和获取播放时长位置
设置或获取播放时长位置,通过绝对时长来表示,单位为毫秒,在设置跳转到指定时长位置时,如果播放器媒体不允许跳转,如:直播流,则会失败。
// 获取当前播放时长位置,单位为毫秒 LIBVLC_API libvlc_time_t libvlc_media_player_get_time(libvlc_media_player_t *p_mi); // 设置当前播放时长位置,单位为毫秒 LIBVLC_API void libvlc_media_player_set_time(libvlc_media_player_t *p_mi, libvlc_time_t i_time) -
设置和获取播放时长百分比位置
设置或获取播放时长的百分比位置来表示,同样在设置播放百分比位置时需要播放器媒体可跳转时才生效。
// 设置播放时长的百分比相对位置 LIBVLC_API void libvlc_media_player_set_position(libvlc_media_player_t *p_mi, float f_pos); // 获取播放时长的百分比相对位置 LIBVLC_API float libvlc_media_player_get_position(libvlc_media_player_t *p_mi); -
设置或获取播放状态
// 获取播放状态,通过枚举值libvlc_state_t进行返回 LIBVLC_API libvlc_state_t libvlc_media_player_get_state(libvlc_media_player_t *p_mi) // 枚举值libvlc_state_t状态包括: typedef enum libvlc_state_t { libvlc_NothingSpecial = 0, // 初始状态,无操作 libvlc_Opening, // 正在打开媒体文件/流 libvlc_Buffering, // 正在缓冲数据 libvlc_Playing, // 正在播放 libvlc_Paused, // 已暂停 libvlc_Stopped, // 已停止 libvlc_Ended, // 播放自然结束 libvlc_Error // 发生错误 } libvlc_state_t; // 检查当前媒体的播放器是否允许跳转,返回1表示允许跳转,返回0表示不允许跳转 LIBVLC_API int libvlc_media_player_is_seekable(libvlc_media_player_t *p_mi) // 检查当前媒体的播放器是否正在播放,返回1表示正在播放,返回0表示没有 static inline int libvlc_media_player_is_playing(libvlc_media_player_t *p_mi) // 设置当前媒体的播放器是否暂停的播放状态,设置为1时表示暂停,设置为0时表示播放 void libvlc_media_player_set_pause(libvlc_media_player_t *p_mi, int do_pause)
4.2.4 事件监听
libvlc库允许监听播放器事件并执行相应绑定的回调函数进行处理。
-
获取事件管理器
// 获取指定播放器的事件管理器,失败返回NULL LIBVLC_API libvlc_event_manager_t * libvlc_media_player_event_manager(libvlc_media_player_t *p_mi) -
注册事件监听和相应处理函数
// 注册播放器的监听事件和相应回调函数,成功返回0,失败返回-1 LIBVLC_API int libvlc_event_attach( libvlc_event_manager_t *p_event_manager, libvlc_event_type_t i_event_type, libvlc_callback_t f_callback, void *user_data )i_event_type:发生的事件类型,包括:typedef enum libvlc_event_e { libvlc_MediaPlayerMediaChanged = 0x100, // 播放器媒体源改变事件 libvlc_MediaPlayerNothingSpecial, // 播放器已创建但未开始播放 libvlc_MediaPlayerOpening, // 媒体正在打开 // 网络流或大文件播放时的缓冲状态变化,可通过 event.u.media_player_buffering.new_cache 获取缓冲百分比 libvlc_MediaPlayerBuffering, libvlc_MediaPlayerPlaying, // 播放器从停止/暂停状态进入播放状态 libvlc_MediaPlayerPaused, // 播放器从播放状态进入暂停状态 libvlc_MediaPlayerStopped, // 播放器停止播放,可能是用户调用 stop() 或发生错误 libvlc_MediaPlayerEndReached, // 媒体文件播放到结尾,正常结束 libvlc_MediaPlayerEncounteredError, // 播放过程中发生错误,如文件损坏、网络中断等 // 播放时间改变,可通过 event.u.media_player_time_changed.new_time 获取新时间(毫秒) libvlc_MediaPlayerTimeChanged, // 播放位置改变,可通过 event.u.media_player_position_changed.new_position 获取新位置 libvlc_MediaPlayerPositionChanged, libvlc_MediaPlayerTitleChanged, // 媒体标题改变 } libvlc_event_e;f_callback:监听事件相应的处理回调函数,类型为:typedef void (*libvlc_callback_t)(const struct libvlc_event_t *event, void *data); // event表示事件 // 事件结构 typedef struct libvlc_event_t { int type; // 事件类型 void *p_obj; // 事件源对象 void *p_user_data; // 用户数据 union { // 媒体播放器事件数据 struct { libvlc_time_t new_time; // 新时间 } media_player_time_changed; struct { float new_position; // 新位置 } media_player_position_changed; struct { int new_cache; // 缓存百分比 } media_player_buffering; struct { libvlc_media_t *new_media; // 新媒体 } media_player_media_changed; // 媒体事件数据 struct { libvlc_meta_t meta_type; // 元数据类型 } media_meta_changed; struct { libvlc_time_t new_duration; // 新时长 } media_duration_changed; struct { int new_status; // 解析状态 } media_parsed_changed; struct { libvlc_state_t new_state; // 新状态 } media_state_changed; } u; } libvlc_event_t;user_data:传递给回调函数f_callback的用户数据
简单的播放器示例代码
#include <iostream>
#include <cstdlib>
#include <vlc/vlc.h>
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cout << "用法: " << argv[0] << " <媒体文件路径>" << std::endl;
return 1;
}
// 1. 初始化 libVLC
libvlc_instance_t* vlcInstance = libvlc_new(0, nullptr);
if (!vlcInstance) {
std::cerr << "无法初始化 libVLC" << std::endl;
return 1;
}
// 2. 创建媒体
libvlc_media_t* media = libvlc_media_new_path(vlcInstance, argv[1]);
if (!media) {
std::cerr << "无法创建媒体" << std::endl;
libvlc_release(vlcInstance);
return 1;
}
// 3. 创建媒体播放器
libvlc_media_player_t* player = libvlc_media_player_new_from_media(media);
if (!player) {
std::cerr << "无法创建播放器" << std::endl;
libvlc_media_release(media);
libvlc_release(vlcInstance);
return 1;
}
// 4. 释放媒体(播放器会持有引用)
libvlc_media_release(media);
// 5. 播放
libvlc_media_player_play(player);
std::cout << "正在播放: " << argv[1] << std::endl;
std::cout << "按回车键停止播放..." << std::endl;
// 等待用户输入
std::cin.get();
// 6. 停止播放
libvlc_media_player_stop(player);
// 7. 释放资源
libvlc_media_player_release(player);
libvlc_release(vlcInstance);
return 0;
}
结语
VLC 库将一款顶级播放器的核心能力以 SDK 的形式开放,极大地降低了开发复杂多媒体应用的难度。无论你是想为自己的应用添加播放功能,还是构建一个流媒体中间件,VLC 库都是一个值得优先考虑的、强大而可靠的选择。
开始你的探索吧,访问 VideoLAN 官方网站获取源码、文档和 SDK,用代码打开音视频世界的大门!想直接获取编译后的libvlc库的头文件、库文件和插件去使用的可以在评论区说明,我发送链接。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)