Commit 772fccba by xiongguangjie

Merge branch 'master' into dev

parents 11fa293a 599208dd
--- ---
name: bug 反馈 name: bug 反馈
about: 反馈 ZLMediaKit 代码本身的 bug about: 反馈 ZLMediaKit 代码本身的 bug
title: "[BUG]: BUG 现象描述" title: "[BUG] BUG现象描述(必填)"
labels: bug labels: bug
assignees: '' assignees: ''
......
--- ---
name: 编译问题反馈 name: 编译问题反馈
about: 反馈 ZLMediaKit 编译相关的问题 about: 反馈 ZLMediaKit 编译相关的问题
title: "[编译问题]: " title: "[编译问题] 编译问题描述(必填)"
labels: 编译问题 labels: 编译问题
assignees: '' assignees: ''
......
--- ---
name: 新增功能请求 name: 新增功能请求
about: 请求新增某些新功能或新特性,或者对已有功能的改进 about: 请求新增某些新功能或新特性,或者对已有功能的改进
title: "[功能请求]" title: "[功能请求] 需求描述(必填)"
labels: 意见建议 labels: 意见建议
assignees: '' assignees: ''
......
--- ---
name: 技术咨询 name: 技术咨询
about: 使用咨询、技术咨询等 about: 使用咨询、技术咨询等
title: "[技术咨询]" title: "[技术咨询] 咨询描述(必填)"
labels: 技术咨询 labels: 技术咨询
assignees: '' assignees: ''
...@@ -16,4 +16,4 @@ assignees: '' ...@@ -16,4 +16,4 @@ assignees: ''
**注意事项** **注意事项**
- 技术咨询前请先认真阅读readme, [wiki](https://github.com/xia-chu/ZLMediaKit/wiki),如有必要,您也可以同时搜索已经答复的issue,如果没找到答案才在此提issue - 技术咨询前请先认真阅读readme, [wiki](https://github.com/xia-chu/ZLMediaKit/wiki),如有必要,您也可以同时搜索已经答复的issue,如果没找到答案才在此提issue
- 技术咨询不属于bug缺陷,建议先star本项目,否则可能会降低答复优先级 - 技术咨询不属于bug缺陷,要求用户先star(收藏)本项目,否则会直接关闭issue
ZLToolKit @ ca26e43a
Subproject commit aea48a14f9619d292789b6ba66d2922e1ed36e71 Subproject commit ca26e43a5f62986bb8a007226e0bad148d154abc
media-server @ 5aa98846
Subproject commit 539f579d59b39b386d8f2d3b59df8f56f9946025 Subproject commit 5aa9884660df1c193d730a90835af36ee411668c
...@@ -66,4 +66,8 @@ WuPeng <wp@zafu.edu.cn> ...@@ -66,4 +66,8 @@ WuPeng <wp@zafu.edu.cn>
[KevinZang](https://github.com/ZSC714725) [KevinZang](https://github.com/ZSC714725)
[gongluck](https://github.com/gongluck) [gongluck](https://github.com/gongluck)
[a-ucontrol](https://github.com/a-ucontrol) [a-ucontrol](https://github.com/a-ucontrol)
[TalusL](https://github.com/TalusL) [TalusL](https://github.com/TalusL)
\ No newline at end of file [ahaooahaz](https://github.com/AHAOAHA)
[TempoTian](https://github.com/TempoTian)
[Derek Liu](https://github.com/yjkhtddx)
[ljx0305](https://github.com/ljx0305)
\ No newline at end of file
...@@ -198,7 +198,7 @@ bash build_docker_images.sh ...@@ -198,7 +198,7 @@ bash build_docker_images.sh
## 联系方式 ## 联系方式
- 邮箱:<1213642868@qq.com>(本项目相关或流媒体相关问题请走issue流程,否则恕不邮件答复) - 邮箱:<1213642868@qq.com>(本项目相关或流媒体相关问题请走issue流程,否则恕不邮件答复)
- QQ群:qq群号在wiki中,请阅读wiki后再加群 - QQ群:两个qq群已满员(共4000人),后续将不再新建qq群,用户可加入[知识星球](https://t.zsxq.com/0cVcuquPJ)提问以支持本项目。
## 怎么提问? ## 怎么提问?
...@@ -295,6 +295,10 @@ bash build_docker_images.sh ...@@ -295,6 +295,10 @@ bash build_docker_images.sh
[gongluck](https://github.com/gongluck) [gongluck](https://github.com/gongluck)
[a-ucontrol](https://github.com/a-ucontrol) [a-ucontrol](https://github.com/a-ucontrol)
[TalusL](https://github.com/TalusL) [TalusL](https://github.com/TalusL)
[ahaooahaz](https://github.com/AHAOAHA)
[TempoTian](https://github.com/TempoTian)
[Derek Liu](https://github.com/yjkhtddx)
[ljx0305](https://github.com/ljx0305)
## 使用案例 ## 使用案例
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
# endif # endif
# endif # endif
#elif !defined(GENERATE_EXPORT) #elif !defined(GENERATE_EXPORT)
# define API_EXPORT # define API_EXPORT __attribute__((visibility("default")))
#endif #endif
#ifdef __cplusplus #ifdef __cplusplus
......
...@@ -537,7 +537,7 @@ void addStreamProxy(const string &vhost, const string &app, const string &stream ...@@ -537,7 +537,7 @@ void addStreamProxy(const string &vhost, const string &app, const string &stream
return; return;
} }
//添加拉流代理 //添加拉流代理
auto player = std::make_shared<PlayerProxy>(vhost, app, stream, option, retry_count >=0 ? retry_count : -1); auto player = std::make_shared<PlayerProxy>(vhost, app, stream, option, retry_count);
s_proxyMap[key] = player; s_proxyMap[key] = player;
//指定RTP over TCP(播放rtsp时有效) //指定RTP over TCP(播放rtsp时有效)
...@@ -952,7 +952,7 @@ void installWebApi() { ...@@ -952,7 +952,7 @@ void installWebApi() {
} }
//添加推流代理 //添加推流代理
PusherProxy::Ptr pusher(new PusherProxy(src, retry_count>=0 ? retry_count : -1)); auto pusher = std::make_shared<PusherProxy>(src, retry_count);
s_proxyPusherMap[key] = pusher; s_proxyPusherMap[key] = pusher;
//指定RTP over TCP(播放rtsp时有效) //指定RTP over TCP(播放rtsp时有效)
...@@ -988,7 +988,7 @@ void installWebApi() { ...@@ -988,7 +988,7 @@ void installWebApi() {
CHECK_SECRET(); CHECK_SECRET();
CHECK_ARGS("schema", "vhost", "app", "stream", "dst_url"); CHECK_ARGS("schema", "vhost", "app", "stream", "dst_url");
auto dst_url = allArgs["dst_url"]; auto dst_url = allArgs["dst_url"];
auto retry_count = allArgs["retry_count"].empty()? -1: allArgs["retry_count"].as<int>(); auto retry_count = allArgs["retry_count"].empty() ? -1 : allArgs["retry_count"].as<int>();
addStreamPusherProxy(allArgs["schema"], addStreamPusherProxy(allArgs["schema"],
allArgs["vhost"], allArgs["vhost"],
allArgs["app"], allArgs["app"],
...@@ -1327,7 +1327,7 @@ void installWebApi() { ...@@ -1327,7 +1327,7 @@ void installWebApi() {
invoker(200, headerOut, val.toStyledString()); invoker(200, headerOut, val.toStyledString());
}); });
}); });
//设置录像流播放速度 //设置录像流播放速度
api_regist("/index/api/setRecordSpeed", [](API_ARGS_MAP_ASYNC) { api_regist("/index/api/setRecordSpeed", [](API_ARGS_MAP_ASYNC) {
CHECK_SECRET(); CHECK_SECRET();
...@@ -1407,7 +1407,7 @@ void installWebApi() { ...@@ -1407,7 +1407,7 @@ void installWebApi() {
invoker(200, headerOut, val.toStyledString()); invoker(200, headerOut, val.toStyledString());
}); });
}); });
// 删除录像文件夹 // 删除录像文件夹
// http://127.0.0.1/index/api/deleteRecordDirectroy?vhost=__defaultVhost__&app=live&stream=ss&period=2020-01-01 // http://127.0.0.1/index/api/deleteRecordDirectroy?vhost=__defaultVhost__&app=live&stream=ss&period=2020-01-01
api_regist("/index/api/deleteRecordDirectory", [](API_ARGS_MAP) { api_regist("/index/api/deleteRecordDirectory", [](API_ARGS_MAP) {
...@@ -1424,7 +1424,7 @@ void installWebApi() { ...@@ -1424,7 +1424,7 @@ void installWebApi() {
val["path"] = record_path; val["path"] = record_path;
val["code"] = result; val["code"] = result;
}); });
//获取录像文件夹列表或mp4文件列表 //获取录像文件夹列表或mp4文件列表
//http://127.0.0.1/index/api/getMp4RecordFile?vhost=__defaultVhost__&app=live&stream=ss&period=2020-01 //http://127.0.0.1/index/api/getMp4RecordFile?vhost=__defaultVhost__&app=live&stream=ss&period=2020-01
api_regist("/index/api/getMp4RecordFile", [](API_ARGS_MAP){ api_regist("/index/api/getMp4RecordFile", [](API_ARGS_MAP){
......
...@@ -161,7 +161,7 @@ void do_http_hook(const string &url, const ArgsType &body, const function<void(c ...@@ -161,7 +161,7 @@ void do_http_hook(const string &url, const ArgsType &body, const function<void(c
GET_CONFIG(float, retry_delay, Hook::kRetryDelay); GET_CONFIG(float, retry_delay, Hook::kRetryDelay);
const_cast<ArgsType &>(body)["mediaServerId"] = mediaServerId; const_cast<ArgsType &>(body)["mediaServerId"] = mediaServerId;
HttpRequester::Ptr requester(new HttpRequester); auto requester = std::make_shared<HttpRequester>();
requester->setMethod("POST"); requester->setMethod("POST");
auto bodyStr = to_string(body); auto bodyStr = to_string(body);
requester->setBody(bodyStr); requester->setBody(bodyStr);
......
...@@ -108,7 +108,7 @@ onceToken token1([](){ ...@@ -108,7 +108,7 @@ onceToken token1([](){
class CMD_main : public CMD { class CMD_main : public CMD {
public: public:
CMD_main() { CMD_main() {
_parser.reset(new OptionParser(nullptr)); _parser = std::make_shared<OptionParser>(nullptr);
#if !defined(_WIN32) #if !defined(_WIN32)
(*_parser) << Option('d',/*该选项简称,如果是\x00则说明无简称*/ (*_parser) << Option('d',/*该选项简称,如果是\x00则说明无简称*/
......
...@@ -98,17 +98,14 @@ bool HlsParser::parse(const string &http_url, const string &m3u8) { ...@@ -98,17 +98,14 @@ bool HlsParser::parse(const string &http_url, const string &m3u8) {
continue; continue;
} }
if (_is_m3u8) { return _is_m3u8 && onParsed(_is_m3u8_inner, _sequence, ts_map);
onParsed(_is_m3u8_inner, _sequence, ts_map);
}
return _is_m3u8;
} }
bool HlsParser::isM3u8() const { bool HlsParser::isM3u8() const {
return _is_m3u8; return _is_m3u8;
} }
bool HlsParser::isLive() const{ bool HlsParser::isLive() const {
return _is_live; return _is_live;
} }
......
...@@ -36,8 +36,9 @@ typedef struct{ ...@@ -36,8 +36,9 @@ typedef struct{
class HlsParser { class HlsParser {
public: public:
HlsParser(){} HlsParser() = default;
~HlsParser(){} ~HlsParser() = default;
bool parse(const std::string &http_url,const std::string &m3u8); bool parse(const std::string &http_url,const std::string &m3u8);
/** /**
...@@ -79,10 +80,16 @@ public: ...@@ -79,10 +80,16 @@ public:
* 得到总时间 * 得到总时间
*/ */
float getTotalDuration() const; float getTotalDuration() const;
protected: protected:
//解析出ts文件地址回调 /**
virtual void onParsed(bool is_m3u8_inner,int64_t sequence,const std::map<int,ts_segment> &ts_list) {}; * 解析m3u8文件回调
* @param is_m3u8_inner 该m3u8文件中是否包含多个hls地址
* @param sequence ts序号
* @param ts_list ts地址列表
* @return 是否解析成功,返回false时,将导致HlsParser::parse返回false
*/
virtual bool onParsed(bool is_m3u8_inner, int64_t sequence, const std::map<int, ts_segment> &ts_list) = 0;
private: private:
bool _is_m3u8 = false; bool _is_m3u8 = false;
......
...@@ -51,7 +51,7 @@ void HlsPlayer::teardown_l(const SockException &ex) { ...@@ -51,7 +51,7 @@ void HlsPlayer::teardown_l(const SockException &ex) {
} else { } else {
_try_fetch_index_times += 1; _try_fetch_index_times += 1;
shutdown(ex); shutdown(ex);
WarnL << "重新尝试拉取索引文件[" << _try_fetch_index_times << "]:" << _play_url; WarnL << "Attempt to pull the m3u8 file again[" << _try_fetch_index_times << "]:" << _play_url;
fetchIndexFile(); fetchIndexFile();
return; return;
} }
...@@ -118,7 +118,7 @@ void HlsPlayer::fetchSegment() { ...@@ -118,7 +118,7 @@ void HlsPlayer::fetchSegment() {
return; return;
} }
if (err) { if (err) {
WarnL << "download ts segment " << url << " failed:" << err.what(); WarnL << "Download ts segment " << url << " failed:" << err.what();
if (err.getErrCode() == Err_timeout) { if (err.getErrCode() == Err_timeout) {
strong_self->_timeout_multiple = MAX(strong_self->_timeout_multiple + 1, MAX_TIMEOUT_MULTIPLE); strong_self->_timeout_multiple = MAX(strong_self->_timeout_multiple + 1, MAX_TIMEOUT_MULTIPLE);
}else{ }else{
...@@ -147,30 +147,41 @@ void HlsPlayer::fetchSegment() { ...@@ -147,30 +147,41 @@ void HlsPlayer::fetchSegment() {
_http_ts_player->sendRequest(url); _http_ts_player->sendRequest(url);
} }
void HlsPlayer::onParsed(bool is_m3u8_inner, int64_t sequence, const map<int, ts_segment> &ts_map) { bool HlsPlayer::onParsed(bool is_m3u8_inner, int64_t sequence, const map<int, ts_segment> &ts_map) {
if (!is_m3u8_inner) { if (!is_m3u8_inner) {
//这是ts播放列表 // 这是ts播放列表
if (_last_sequence == sequence) { if (_last_sequence == sequence) {
return; // 如果是重复的ts列表,那么忽略
// 但是需要注意, 如果当前ts列表为空了, 那么表明直播结束了或者m3u8文件有问题,需要重新拉流
// 这里的5倍是为了防止m3u8文件有问题导致的无限重试
if (_last_sequence > 0 && _ts_list.empty() && HlsParser::isLive()
&& _wait_index_update_ticker.elapsedTime() > (uint64_t)HlsParser::getTargetDur() * 1000 * 5) {
_wait_index_update_ticker.resetTime();
WarnL << "Fetch new ts list from m3u8 timeout";
return false;
}
return true;
} }
_last_sequence = sequence; _last_sequence = sequence;
_wait_index_update_ticker.resetTime();
for (auto &pr : ts_map) { for (auto &pr : ts_map) {
auto &ts = pr.second; auto &ts = pr.second;
if (_ts_url_cache.emplace(ts.url).second) { if (_ts_url_cache.emplace(ts.url).second) {
//该ts未重复 // 该ts未重复
_ts_list.emplace_back(ts); _ts_list.emplace_back(ts);
//按时间排序 // 按时间排序
_ts_url_sort.emplace_back(ts.url); _ts_url_sort.emplace_back(ts.url);
} }
} }
if (_ts_url_sort.size() > 2 * ts_map.size()) { if (_ts_url_sort.size() > 2 * ts_map.size()) {
//去除防重列表中过多的数据 // 去除防重列表中过多的数据
_ts_url_cache.erase(_ts_url_sort.front()); _ts_url_cache.erase(_ts_url_sort.front());
_ts_url_sort.pop_front(); _ts_url_sort.pop_front();
} }
fetchSegment(); fetchSegment();
} else { } else {
//这是m3u8列表,我们播放最高清的子hls // 这是m3u8列表,我们播放最高清的子hls
if (ts_map.empty()) { if (ts_map.empty()) {
throw invalid_argument("empty sub hls list:" + getUrl()); throw invalid_argument("empty sub hls list:" + getUrl());
} }
...@@ -184,6 +195,7 @@ void HlsPlayer::onParsed(bool is_m3u8_inner, int64_t sequence, const map<int, ts ...@@ -184,6 +195,7 @@ void HlsPlayer::onParsed(bool is_m3u8_inner, int64_t sequence, const map<int, ts
} }
}, false); }, false);
} }
return true;
} }
void HlsPlayer::onResponseHeader(const string &status, const HttpClient::HttpHeader &headers) { void HlsPlayer::onResponseHeader(const string &status, const HttpClient::HttpHeader &headers) {
...@@ -193,7 +205,7 @@ void HlsPlayer::onResponseHeader(const string &status, const HttpClient::HttpHea ...@@ -193,7 +205,7 @@ void HlsPlayer::onResponseHeader(const string &status, const HttpClient::HttpHea
} }
auto content_type = strToLower(const_cast<HttpClient::HttpHeader &>(headers)["Content-Type"]); auto content_type = strToLower(const_cast<HttpClient::HttpHeader &>(headers)["Content-Type"]);
if (content_type.find("application/vnd.apple.mpegurl") != 0 && content_type.find("/x-mpegurl") == _StrPrinter::npos) { if (content_type.find("application/vnd.apple.mpegurl") != 0 && content_type.find("/x-mpegurl") == _StrPrinter::npos) {
WarnL << "may not a hls video: " << content_type << ", url: " << getUrl(); WarnL << "May not a hls video: " << content_type << ", url: " << getUrl();
} }
_m3u8.clear(); _m3u8.clear();
} }
...@@ -208,7 +220,7 @@ void HlsPlayer::onResponseCompleted(const SockException &ex) { ...@@ -208,7 +220,7 @@ void HlsPlayer::onResponseCompleted(const SockException &ex) {
return; return;
} }
if (!HlsParser::parse(getUrl(), _m3u8)) { if (!HlsParser::parse(getUrl(), _m3u8)) {
teardown_l(SockException(Err_other, "parse m3u8 failed:" + _m3u8)); teardown_l(SockException(Err_other, "parse m3u8 failed:" + _play_url));
return; return;
} }
if (!_play_result) { if (!_play_result) {
......
...@@ -73,11 +73,11 @@ protected: ...@@ -73,11 +73,11 @@ protected:
virtual void onPacket(const char *data, size_t len) = 0; virtual void onPacket(const char *data, size_t len) = 0;
private: private:
void onParsed(bool is_m3u8_inner,int64_t sequence,const map<int,ts_segment> &ts_map) override; bool onParsed(bool is_m3u8_inner, int64_t sequence, const map<int, ts_segment> &ts_map) override;
void onResponseHeader(const std::string &status,const HttpHeader &headers) override; void onResponseHeader(const std::string &status, const HttpHeader &headers) override;
void onResponseBody(const char *buf,size_t size) override; void onResponseBody(const char *buf, size_t size) override;
void onResponseCompleted(const toolkit::SockException &e) override; void onResponseCompleted(const toolkit::SockException &e) override;
bool onRedirectUrl(const std::string &url,bool temporary) override; bool onRedirectUrl(const std::string &url, bool temporary) override;
private: private:
void playDelay(); void playDelay();
...@@ -101,6 +101,7 @@ private: ...@@ -101,6 +101,7 @@ private:
std::string _play_url; std::string _play_url;
toolkit::Timer::Ptr _timer; toolkit::Timer::Ptr _timer;
toolkit::Timer::Ptr _timer_ts; toolkit::Timer::Ptr _timer_ts;
toolkit::Ticker _wait_index_update_ticker;
std::list<ts_segment> _ts_list; std::list<ts_segment> _ts_list;
std::list<std::string> _ts_url_sort; std::list<std::string> _ts_url_sort;
std::set<std::string, UrlComp> _ts_url_cache; std::set<std::string, UrlComp> _ts_url_cache;
......
...@@ -271,7 +271,7 @@ static void sendReport() { ...@@ -271,7 +271,7 @@ static void sendReport() {
} }
static toolkit::onceToken s_token([]() { static toolkit::onceToken s_token([]() {
NoticeCenter::Instance().addListener(nullptr, EventPollerPool::kOnStarted, [](EventPollerPool &pool, size_t &size) { NoticeCenter::Instance().addListener(nullptr, "kBroadcastEventPollerPoolStarted", [](EventPollerPool &pool, size_t &size) {
// 第一次汇报在程序启动后5分钟 // 第一次汇报在程序启动后5分钟
pool.getPoller()->doDelayTask(5 * 60 * 1000, []() { pool.getPoller()->doDelayTask(5 * 60 * 1000, []() {
sendReport(); sendReport();
......
...@@ -246,6 +246,10 @@ void RtpSender::onConnect(){ ...@@ -246,6 +246,10 @@ void RtpSender::onConnect(){
} }
bool RtpSender::addTrack(const Track::Ptr &track){ bool RtpSender::addTrack(const Track::Ptr &track){
if (_args.only_audio && track->getTrackType() == TrackVideo) {
// 如果只发送音频则忽略视频
return false;
}
return _interface->addTrack(track); return _interface->addTrack(track);
} }
...@@ -265,6 +269,10 @@ void RtpSender::flush() { ...@@ -265,6 +269,10 @@ void RtpSender::flush() {
//此函数在其他线程执行 //此函数在其他线程执行
bool RtpSender::inputFrame(const Frame::Ptr &frame) { bool RtpSender::inputFrame(const Frame::Ptr &frame) {
if (_args.only_audio && frame->getTrackType() == TrackVideo) {
// 如果只发送音频则忽略视频
return false;
}
//连接成功后才做实质操作(节省cpu资源) //连接成功后才做实质操作(节省cpu资源)
return _is_connect ? _interface->inputFrame(frame) : false; return _is_connect ? _interface->inputFrame(frame) : false;
} }
......
...@@ -18,11 +18,19 @@ ...@@ -18,11 +18,19 @@
</video> </video>
</div> </div>
<div> <div style="float: left; width:30%;">
<span>已存在的流列表,点击自动播放:</span>
<ol id="olstreamlist">
<li>初始演示</li>
<li>每秒自动刷新</li>
</ol>
</div>
<div style="float: right; width: 70%">
<p> <p>
<label for="streamUrl">url:</label> <label for="streamUrl">url:</label>
<input type="text" style="co" id='streamUrl' value="http://192.168.1.101/index/api/webrtc?app=live&stream=xiong&type=play"> <input type="text" style="co; width:70%" id='streamUrl' value="http://192.168.1.101/index/api/webrtc?app=live&stream=xiong&type=play">
</p> </p>
<p> <p>
...@@ -252,6 +260,64 @@ ...@@ -252,6 +260,64 @@
} }
} }
function on_click_to_play(app, stream) {
console.log(`on_click_to_play: ${app}/${stream}`);
var url = `${document.location.protocol}//${window.location.host}/index/api/webrtc?app=${app}&stream=${stream}&type=play`;
document.getElementById('streamUrl').value = url;
start();
}
function clearStreamList() {
let content = document.getElementById("olstreamlist");
while (content.hasChildNodes()) {
content.removeChild(content.firstChild);
}
}
function fillStreamList(json) {
clearStreamList();
if (json.code != 0) {
return;
}
let ss = {};
for (let o of json.data) {
if (ss[o.app]) {
ss[o.app].add(o.stream);
} else {
let set = new Set();
set.add(o.steram);
ss[o.app] = set;
}
}
for (let o in ss) {
let app = o;
for (let s of ss[o]) {
if (s) {
//console.log(app, s);
let content = document.getElementById("olstreamlist");
let child = `<li app="${app}" stream="${s}" onmouseover="this.style.color='blue';" onclick="on_click_to_play('${app}', '${s}')">${app}/${s}</li>`;
content.insertAdjacentHTML("beforeend", child);
}
}
}
}
async function getData(url) {
const response = await fetch(url, {
method: 'GET'
});
const data = await response.json();
//console.log(data);
return data;
}
function get_media_list() {
let url = document.location.protocol+"//"+window.location.host+"/index/api/getMediaList?secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc";
let json = getData(url);
json.then((json)=> fillStreamList(json));
}
setInterval(() => {
get_media_list();
}, 1000);
</script> </script>
</body> </body>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论