diff --git a/library/borealis b/library/borealis index e6eb4158..803eb868 160000 --- a/library/borealis +++ b/library/borealis @@ -1 +1 @@ -Subproject commit e6eb415820628b7cb5553111da53a8a19e19e78c +Subproject commit 803eb8681b6ac9d51482fda9f6adf2ffe8d4a887 diff --git a/resources/i18n/en-US/wiliwili.json b/resources/i18n/en-US/wiliwili.json index 7ff389b7..3f86fce5 100644 --- a/resources/i18n/en-US/wiliwili.json +++ b/resources/i18n/en-US/wiliwili.json @@ -329,6 +329,7 @@ }, "filter": { "header": "Danmaku filter", + "mask": "Smart mask", "top": "Top", "scroll": "Scroll", "bottom": "Bottom", diff --git a/resources/i18n/zh-Hans/wiliwili.json b/resources/i18n/zh-Hans/wiliwili.json index 0d530810..f3b02a4b 100644 --- a/resources/i18n/zh-Hans/wiliwili.json +++ b/resources/i18n/zh-Hans/wiliwili.json @@ -329,6 +329,7 @@ }, "filter": { "header": "弹幕过滤", + "mask": "智能防挡", "top": "顶部弹幕", "scroll": "滚动弹幕", "bottom": "底部弹幕", diff --git a/resources/i18n/zh-Hant/wiliwili.json b/resources/i18n/zh-Hant/wiliwili.json index 7e2b3e26..5c51aaf8 100644 --- a/resources/i18n/zh-Hant/wiliwili.json +++ b/resources/i18n/zh-Hant/wiliwili.json @@ -329,6 +329,7 @@ }, "filter": { "header": "彈幕篩選", + "mask": "智慧防擋", "top": "頂部", "scroll": "滾動", "bottom": "底部", diff --git a/resources/svg/ico-space-activate.svg b/resources/svg/ico-space-activate.svg new file mode 100644 index 00000000..ec4ac6d1 --- /dev/null +++ b/resources/svg/ico-space-activate.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/resources/svg/ico-space.svg b/resources/svg/ico-space.svg new file mode 100644 index 00000000..a615d3b4 --- /dev/null +++ b/resources/svg/ico-space.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/resources/xml/fragment/player_danmaku_setting.xml b/resources/xml/fragment/player_danmaku_setting.xml index 0dc14f0c..21265aae 100644 --- a/resources/xml/fragment/player_danmaku_setting.xml +++ b/resources/xml/fragment/player_danmaku_setting.xml @@ -84,6 +84,9 @@ + + diff --git a/resources/xml/fragment/space_tab.xml b/resources/xml/fragment/space_tab.xml new file mode 100644 index 00000000..15c727bb --- /dev/null +++ b/resources/xml/fragment/space_tab.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/scripts/psv/Dockerfile b/scripts/psv/Dockerfile new file mode 100644 index 00000000..1ce4af8c --- /dev/null +++ b/scripts/psv/Dockerfile @@ -0,0 +1,65 @@ +FROM vitasdk/vitasdk:latest + +MAINTAINER xfangfang + +RUN apk update && \ + apk add cmake ninja meson pkgconf bash git zstd tar patch && \ + git config --global --add safe.directory $(pwd) + +# Copy PVR_PSP2 (GLES) to vita toolchain dir +RUN mkdir -p /vita/dependencies/include && \ + mkdir -p /vita/dependencies/lib && \ + mkdir -p /vita/dependencies/suprx && \ + pvr_psp2_version=3.9 && \ + wget https://github.com/GrapheneCt/PVR_PSP2/archive/refs/tags/v$pvr_psp2_version.zip -P/tmp && \ + unzip /tmp/v$pvr_psp2_version.zip -d/tmp && \ + cp -r /tmp/PVR_PSP2-$pvr_psp2_version/include/* /vita/dependencies/include && \ + sed -i -e s/__drvkhrplatform_h_/__khrplatform_h_/ /vita/dependencies/include/KHR/khrplatform.h && \ + wget https://github.com/GrapheneCt/PVR_PSP2/releases/download/v$pvr_psp2_version/vitasdk_stubs.zip -P/tmp && \ + unzip /tmp/vitasdk_stubs.zip -d/tmp/pvr_psp2_stubs && \ + find /tmp/pvr_psp2_stubs -type f -name "*.a" -exec cp {} /vita/dependencies/lib \; && \ + wget https://github.com/GrapheneCt/PVR_PSP2/releases/download/v$pvr_psp2_version/PSVita_Release.zip -P/tmp && \ + unzip /tmp/PSVita_Release.zip -d/tmp/PSVita_Release && \ + rm /tmp/PSVita_Release/libGLESv1_CM.suprx && \ + rm /tmp/PSVita_Release/libpvr2d.suprx && \ + mv /tmp/PSVita_Release/*.suprx /vita/dependencies/suprx/ && \ + cp -rv /vita/dependencies/* ${VITASDK}/arm-vita-eabi && \ + rm -rf /vita && \ + rm -rf /tmp/* + +# Install VDPM Dependencies +ADD . /vdpm +RUN vdpm mbedtls libass harfbuzz fribidi freetype libpng libwebp && \ + adduser --gecos '' --disabled-password builder && \ + echo 'builder ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/builder && \ + chown -R builder:builder /vdpm && \ + ls -l /vdpm && \ + su - builder -c "cd /vdpm/ffmpeg && vita-makepkg" && \ + su - builder -c "cd /vdpm/curl && vita-makepkg" && \ + su - builder -c "cd /vdpm/sdl2 && vita-makepkg" && \ + vdpm /vdpm/ffmpeg/*-arm.tar.xz && \ + vdpm /vdpm/sdl2/*-arm.tar.xz && \ + touch /tmp/vdpm_install_ffmpeg && \ + touch /tmp/vdpm_install_sdl2 && \ + su - builder -c "cd /vdpm/mpv && vita-makepkg" && \ + vdpm /vdpm/curl/*-arm.tar.xz && \ + vdpm /vdpm/mpv/*-arm.tar.xz && \ + touch /tmp/vdpm_install_curl && \ + touch /tmp/vdpm_install_mpv && \ + rm -rf /vdpm + +RUN mkdir /src/ &&\ + echo \#\!/bin/bash -i >> /entrypoint.sh &&\ + echo >> /entrypoint.sh &&\ + echo "set -e" >> /entrypoint.sh &&\ + echo "make -p /src/scripts/psv/module/" >> /entrypoint.sh &&\ + echo "cp ${VITASDK}/arm-vita-eabi/suprx/*.suprx /src/scripts/psv/module/" >> /entrypoint.sh &&\ + echo "cd /src" >> /entrypoint.sh &&\ + echo "echo \"\$@\"" >> /entrypoint.sh &&\ + echo "bash -c \"\$@\"" >> /entrypoint.sh &&\ + chmod +x /entrypoint.sh + +VOLUME /src/ +WORKDIR /src/ +SHELL ["/bin/bash", "-i", "-c"] +ENTRYPOINT ["/entrypoint.sh"] diff --git a/scripts/switch/mpv_deko3d/.gitignore b/scripts/switch/mpv_deko3d/.gitignore new file mode 100644 index 00000000..a30cbdd8 --- /dev/null +++ b/scripts/switch/mpv_deko3d/.gitignore @@ -0,0 +1 @@ +mpv.patch \ No newline at end of file diff --git a/wiliwili/include/api/bilibili.h b/wiliwili/include/api/bilibili.h index 8c13de6d..3fddbcbd 100644 --- a/wiliwili/include/api/bilibili.h +++ b/wiliwili/include/api/bilibili.h @@ -75,7 +75,7 @@ using Cookies = std::map; using ErrorCallback = std::function; #define BILI bilibili::BilibiliClient -#define BILI_ERR const std::string& error, int code +#define BILI_ERR const std::string &error, int code class BilibiliClient { inline static std::function @@ -246,6 +246,12 @@ class BilibiliClient { const std::function& callback = nullptr, const ErrorCallback& error = nullptr); + /// 获取视频防遮挡数据 + static void get_webmask( + const std::string& url, + const std::function& callback = nullptr, + const ErrorCallback& error = nullptr); + /// get video pagelist by aid static void get_video_pagelist( int aid, diff --git a/wiliwili/include/api/bilibili/result/video_detail_result.h b/wiliwili/include/api/bilibili/result/video_detail_result.h index ffec91ef..e0ea6631 100644 --- a/wiliwili/include/api/bilibili/result/video_detail_result.h +++ b/wiliwili/include/api/bilibili/result/video_detail_result.h @@ -236,7 +236,8 @@ inline void from_json(const nlohmann::json& nlohmann_json_j, } else { nlohmann_json_t.upper = 0; } - if (nlohmann_json_j.contains("root") && !nlohmann_json_j.at("root").is_null()) { + if (nlohmann_json_j.contains("root") && + !nlohmann_json_j.at("root").is_null()) { nlohmann_json_j.at("root").get_to(nlohmann_json_t.root); } NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, cursor)); @@ -637,6 +638,7 @@ class VideoPageResult { int64_t last_play_time; int last_play_cid; VideoPageSubtitleList subtitles; + std::string mask_url; }; inline void from_json(const nlohmann::json& nlohmann_json_j, VideoPageResult& nlohmann_json_t) { @@ -649,6 +651,12 @@ inline void from_json(const nlohmann::json& nlohmann_json_j, } } } + if (nlohmann_json_j.contains("dm_mask") && + nlohmann_json_j.at("dm_mask").is_object()) { + nlohmann_json_j.at("dm_mask") + .at("mask_url") + .get_to(nlohmann_json_t.mask_url); + } NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, online_count, last_play_time, last_play_cid)); } @@ -705,7 +713,7 @@ class VideoHighlightProgress { std::vector data; }; inline void from_json(const nlohmann::json& nlohmann_json_j, - VideoHighlightProgress& nlohmann_json_t) { + VideoHighlightProgress& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, step_sec)); if (!nlohmann_json_j.contains("events")) return; if (!nlohmann_json_j.at("events").is_object()) return; diff --git a/wiliwili/include/fragment/player_danmaku_setting.hpp b/wiliwili/include/fragment/player_danmaku_setting.hpp index 667faf48..996b2955 100644 --- a/wiliwili/include/fragment/player_danmaku_setting.hpp +++ b/wiliwili/include/fragment/player_danmaku_setting.hpp @@ -30,6 +30,7 @@ class PlayerDanmakuSetting : public brls::Box { private: BRLS_BIND(SelectorCell, cellLevel, "player/danmaku/filter/level"); + BRLS_BIND(brls::BooleanCell, cellMask, "player/danmaku/filter/mask"); BRLS_BIND(brls::BooleanCell, cellScroll, "player/danmaku/filter/scroll"); BRLS_BIND(brls::BooleanCell, cellTop, "player/danmaku/filter/top"); BRLS_BIND(brls::BooleanCell, cellBottom, "player/danmaku/filter/bottom"); diff --git a/wiliwili/include/fragment/space_tab.hpp b/wiliwili/include/fragment/space_tab.hpp new file mode 100644 index 00000000..b510ba29 --- /dev/null +++ b/wiliwili/include/fragment/space_tab.hpp @@ -0,0 +1,48 @@ +// +// Created by fang on 2022/6/14. +// + +// register this fragment in main.cpp +//#include "fragment/home_recommends.hpp" +// brls::Application::registerXMLView("HomeRecommends", HomeRecommends::create); +// inline std::string format(fmt::string_view fmt, Args&&... args) { return fmt::format(fmt::runtime(fmt), std::forward(args)...); } diff --git a/wiliwili/include/view/danmaku_core.hpp b/wiliwili/include/view/danmaku_core.hpp index 6af30343..fba90da9 100644 --- a/wiliwili/include/view/danmaku_core.hpp +++ b/wiliwili/include/view/danmaku_core.hpp @@ -8,9 +8,42 @@ #include -#include #include -#include "nanovg.h" + +// 每个分片内的svg数据,一般 1/30 s 一帧 +class MaskSvg { +public: + MaskSvg(const std::string &svg, uint64_t t) : svg(svg), showTime(t) {} + std::string svg; + uint64_t showTime; +}; + +// 一般 10s 一个分片 +class MaskSlice { +public: + MaskSlice(uint64_t time, uint64_t offsetStart, uint64_t offsetEnd) + : time(time), offsetStart(offsetStart), offsetEnd(offsetEnd) {} + uint64_t time{}; + uint64_t offsetStart{}; + uint64_t offsetEnd{}; + std::vector svgData; +}; + +class WebMask { +public: + std::string url; + uint32_t version, check, length; + std::vector sliceData; + + const MaskSlice &getSlice(size_t index); + + void parse(const std::string &text); + + void clear(); + +private: + std::string rawData; +}; class DanmakuItem { public: @@ -79,8 +112,8 @@ class DanmakuCore : public brls::Singleton { * @param height 绘制区域的高度 * @param alpha 组件的透明度,与弹幕本身的透明度叠加 */ - void draw(NVGcontext *vg, float x, float y, float width, - float height, float alpha); + void draw(NVGcontext *vg, float x, float y, float width, float height, + float alpha); /** * 加载弹幕数据 @@ -100,6 +133,12 @@ class DanmakuCore : public brls::Singleton { */ std::vector getDanmakuData(); + /** + * 加载遮罩数据 + * @param data 遮罩数据 + */ + void loadMaskData(const WebMask &data); + /// range: [1 - 10], 1: show all danmaku, 10: the most strong filter static inline int DANMAKU_FILTER_LEVEL = 1; @@ -107,6 +146,7 @@ class DanmakuCore : public brls::Singleton { static inline bool DANMAKU_FILTER_SHOW_BOTTOM = true; static inline bool DANMAKU_FILTER_SHOW_SCROLL = true; static inline bool DANMAKU_FILTER_SHOW_COLOR = true; + static inline bool DANMAKU_SMART_MASK = true; /// [25, 50, 75, 100] static inline int DANMAKU_STYLE_AREA = 100; @@ -135,6 +175,11 @@ class DanmakuCore : public brls::Singleton { // 弹幕列表 std::vector danmakuData; + WebMask maskData{}; + size_t maskIndex = 0; + size_t maskSliceIndex = 0; + int maskTex = 0; + // 滚动弹幕的信息 <起始时间,结束时间> std::vector> scrollLines; diff --git a/wiliwili/source/api/video_detail_api.cpp b/wiliwili/source/api/video_detail_api.cpp index fc817117..3bd90089 100644 --- a/wiliwili/source/api/video_detail_api.cpp +++ b/wiliwili/source/api/video_detail_api.cpp @@ -50,6 +50,29 @@ void BilibiliClient::get_page_detail( callback, error); } +void BilibiliClient::get_webmask( + const std::string& url, + const std::function& callback, + const ErrorCallback& error) { + cpr::GetCallback<>( + [callback, error](const cpr::Response& r) { + try { + callback(r.text); + } catch (const std::exception& e) { + ERROR_MSG("Network error. [Status code: " + + std::to_string(r.status_code) + " ]", + r.status_code); + printf("data: %s\n", r.text.c_str()); + printf("ERROR: %s\n", e.what()); + } + }, +#ifndef VERIFY_SSL + cpr::VerifySsl{false}, +#endif + cpr::Url{url}, HTTP::HEADERS, HTTP::COOKIES, + cpr::Timeout{HTTP::TIMEOUT}); +} + void BilibiliClient::get_video_pagelist( const std::string& bvid, const std::function& callback, diff --git a/wiliwili/source/fragment/player_danmaku_setting.cpp b/wiliwili/source/fragment/player_danmaku_setting.cpp index 537ac395..159d00c5 100644 --- a/wiliwili/source/fragment/player_danmaku_setting.cpp +++ b/wiliwili/source/fragment/player_danmaku_setting.cpp @@ -34,6 +34,16 @@ PlayerDanmakuSetting::PlayerDanmakuSetting() { auto& conf = ProgramConfig::instance(); +#if defined(BOREALIS_USE_OPENGL) && !defined(__PSV__) + this->cellMask->init("wiliwili/player/danmaku/filter/mask"_i18n, + DanmakuCore::DANMAKU_SMART_MASK, [](bool data) { + DanmakuCore::DANMAKU_SMART_MASK = data; + DanmakuCore::save(); + return true; + }); +#else + this->cellMask->setVisibility(brls::Visibility::GONE); +#endif this->cellTop->init("wiliwili/player/danmaku/filter/top"_i18n, DanmakuCore::DANMAKU_FILTER_SHOW_TOP, [](bool data) { DanmakuCore::DANMAKU_FILTER_SHOW_TOP = data; diff --git a/wiliwili/source/fragment/space_tab.cpp b/wiliwili/source/fragment/space_tab.cpp new file mode 100644 index 00000000..8e05d1aa --- /dev/null +++ b/wiliwili/source/fragment/space_tab.cpp @@ -0,0 +1,176 @@ +// +// Created by fang on 2022/6/14. +// + +#include "fragment/space_tab.hpp" + +#include +#include "view/mpv_core.hpp" +#include "view/recycling_grid.hpp" +#include "view/video_card.hpp" +#include "utils/number_helper.hpp" +#include "utils/activity_helper.hpp" +#include "utils/image_helper.hpp" + +using namespace brls::literals; + +/// SpaceVideoCard +//class SpaceVideoCard : public BaseVideoCard { +//public: +// SpaceVideoCard() { +// this->inflateFromXMLRes("xml/views/video_card.xml"); +// } +// +// ~SpaceVideoCard() override; +// +// void setCard(std::string pic, std::string title, std::string username, +// int pubdate = 0, int view_count = 0, int danmaku = 0, +// std::string rightBottomBadge = "", std::string extra = ""); +// +// static SpaceVideoCard* create(); +// +//private: +// BRLS_BIND(TextBox, labelTitle, "video/card/label/title"); +// BRLS_BIND(brls::Label, labelUsername, "video/card/label/username"); +// BRLS_BIND(brls::Label, labelCount, "video/card/label/count"); +// BRLS_BIND(brls::Label, labelDanmaku, "video/card/label/danmaku"); +// BRLS_BIND(brls::Label, labelDuration, "video/card/label/duration"); +// BRLS_BIND(brls::Box, boxPic, "video/card/pic_box"); +// BRLS_BIND(brls::Box, boxHint, "video/card/hint"); +// BRLS_BIND(brls::Label, labelHint, "video/card/label/hint"); +// BRLS_BIND(SVGImage, svgUp, "video/svg/up"); +// BRLS_BIND(brls::Box, boxRCMD, "video/card/rcmd_box"); +// BRLS_BIND(brls::Label, labelRCMD, "video/card/label/rcmd"); +// BRLS_BIND(brls::Box, boxAchievement, "video/card/achievement_box"); +// BRLS_BIND(brls::Label, labelAchievement, "video/card/label/achievement"); +//}; + +/// DataSourceSpaceVideoList + +class DataSourceSpaceVideoList : public RecyclingGridDataSource { +public: + explicit DataSourceSpaceVideoList( + bilibili::RecommendVideoListResult result) + : recommendList(std::move(result)) {} + RecyclingGridItem* cellForRow(RecyclingGrid* recycler, + size_t index) override { + //从缓存列表中取出 或者 新生成一个表单项 + RecyclingGridItemVideoCard* item = + (RecyclingGridItemVideoCard*)recycler->dequeueReusableCell("Cell"); + + bilibili::RecommendVideoResult& r = this->recommendList[index]; + item->setHideHighlightBorder(true); + item->setHideHighlightBackground(true); + item->setCard(r.pic + ImageHelper::h_ext, r.title, r.owner.name, + r.pubdate, r.stat.view, r.stat.danmaku, r.duration, + r.rcmd_reason.content); + return item; + } + + size_t getItemCount() override { return recommendList.size(); } + + void onItemSelected(RecyclingGrid* recycler, size_t index) override { + Intent::openBV(recommendList[index].bvid); + } + + void appendData(const bilibili::RecommendVideoListResult& data) { + //todo: 研究一下多线程条件下的问题 + //todo: 性能更强地去重 + brls::Logger::debug("DataSourceRecommendVideoList: append data"); + bool skip = false; + for (const auto& i : data) { + skip = false; + for (const auto& j : this->recommendList) { + if (j.cid == i.cid) { + skip = true; + break; + } + } + if (!skip) { + this->recommendList.push_back(i); + } + } + } + + void clearData() override { this->recommendList.clear(); } + +private: + bilibili::RecommendVideoListResult recommendList; +}; + +/// HomeRecommends + +SpaceTab::SpaceTab() { + this->inflateFromXMLRes("xml/fragment/space_tab.xml"); +// recyclingGrid->estimatedRowHeight = brls::Application::contentHeight; +// recyclingGrid->registerCell( +// "Cell", []() { return RecyclingGridItemVideoCard::create(); }); +// recyclingGrid->onNextPage([this]() { this->requestData(); }); +// recyclingGrid->setRefreshAction([this]() { +// brls::Logger::debug("refresh home recommends"); +// AutoTabFrame::focus2Sidebar(this); +// this->recyclingGrid->showSkeleton(); +// this->requestData(true); +// }); + this->requestData(); + +// brls::Application::getWindowSizeChangedEvent()->subscribe([this](){ +// recyclingGrid->estimatedRowHeight = brls::Application::contentHeight; +// recyclingGrid->reloadData(); +// }); +} + +void SpaceTab::onCreate() {} + +void SpaceTab::onLayout() { +// brls::Rect rect = getFrame(); +// if (!(rect == oldRect)) MPVCore::instance().setFrameSize(rect); +// oldRect = rect; +} + +void SpaceTab::onRecommendVideoList( + const bilibili::RecommendVideoListResultWrapper& result) { + brls::Threading::sync([this, result]() { + MPVCore::instance().setUrl("https://www.w3schools.com/html/movie.mp4"); +// auto* datasource = dynamic_cast( +// recyclingGrid->getDataSource()); +// if (datasource && result.requestIndex != 1) { +// brls::Logger::debug("refresh home recommends: auto load {}", +// result.requestIndex); +// datasource->appendData(result.item); +// recyclingGrid->notifyDataChanged(); +// } else { +// brls::Logger::verbose("refresh home recommends: first page"); +// recyclingGrid->setDataSource( +// new DataSourceSpaceVideoList(result.item)); +// } + }); +} + +brls::View* SpaceTab::create() { return new SpaceTab(); } + +void SpaceTab::draw(NVGcontext* vg, float x, float y, float width, float height, brls::Style style, brls::FrameContext* ctx) { + MPVCore::instance().draw(brls::Rect(x,y,width,height), this->getAlpha()); +} + +SpaceTab::~SpaceTab() = default; + +void SpaceTab::onError(const std::string& error) { + brls::sync([this, error]() { +// this->recyclingGrid->setError(error); + }); +} + +void SpaceTab::willDisappear(bool resetState) { + Box::willDisappear(resetState); + brls::Logger::error("SpaceTab::willDisappear"); +// MPVCore::instance().setSkipRender(true); + +} + +void SpaceTab::willAppear(bool resetState) { + // todo:从其他页面返回时重新加载进度 + Box::willAppear(resetState); +// MPVCore::instance().setSkipRender(false); +// MPVCore::instance().setFrameSize(getFrame()); +} \ No newline at end of file diff --git a/wiliwili/source/main.cpp b/wiliwili/source/main.cpp index 6b681a8f..c9df3e5b 100644 --- a/wiliwili/source/main.cpp +++ b/wiliwili/source/main.cpp @@ -53,7 +53,9 @@ int main(int argc, char* argv[]) { if (brls::Application::getPlatform()->isApplicationMode()) { Intent::openMain(); - // Use these activities to debug + // Uncomment these lines to debug activities + // Intent::openBV("BV1Da411Y7U4"); // 弹幕防遮挡 (横屏) + // Intent::openBV("BV1iN4y1m7J3"); // 弹幕防遮挡 (竖屏) // Intent::openBV("BV18W4y1q72C"); // wiliwili介绍 // Intent::openBV("BV1dx411c7Av"); // flv拼接视频 // Intent::openBV("BV15z4y1Z734"); // 4K HDR 视频 diff --git a/wiliwili/source/presenter/video_detail.cpp b/wiliwili/source/presenter/video_detail.cpp index 89b5b88c..de657369 100644 --- a/wiliwili/source/presenter/video_detail.cpp +++ b/wiliwili/source/presenter/video_detail.cpp @@ -598,6 +598,26 @@ void VideoDetail::requestVideoPageDetail(const std::string& bvid, int cid, bvid, cid, [ASYNC_TOKEN, requestVideoHistory](const bilibili::VideoPageResult& result) { +#if defined(BOREALIS_USE_OPENGL) && !defined(__PSV__) + if (!result.mask_url.empty()) { + brls::Logger::debug("获取防遮挡数据: {}", result.mask_url); + auto url = pystring::startswith(result.mask_url, "//") + ? "https:" + result.mask_url + : result.mask_url; + BILI::get_webmask( + url, + [url](const std::string& text) { + brls::Logger::debug("解析防遮挡数据: {}", text.size()); + WebMask webMask; + webMask.url = url; + webMask.parse(text); + brls::Logger::debug("解析数据结束: {}", url); + brls::sync( + [webMask]() { DanmakuCore::instance().loadMaskData(webMask); }); + }, + [](BILI_ERR) { brls::Logger::error("get_webmask: {}", error); }); + } +#endif brls::sync([ASYNC_TOKEN, result, requestVideoHistory]() { ASYNC_RELEASE SubtitleCore::instance().setSubtitleList(result); @@ -663,7 +683,7 @@ void VideoDetail::reportHistory(unsigned int aid, unsigned int cid, BILI::report_history( mid, token, aid, cid, type, progress, duration, sid, epid, []() { brls::Logger::debug("reportHistory: success"); }, - [](BILI_ERR) { brls::Logger::error("{}", error); }); + [](BILI_ERR) { brls::Logger::error("reportHistory: {}", error); }); } int VideoDetail::getCoinTolerate() { diff --git a/wiliwili/source/utils/config_helper.cpp b/wiliwili/source/utils/config_helper.cpp index 97bb92b2..e77249c1 100644 --- a/wiliwili/source/utils/config_helper.cpp +++ b/wiliwili/source/utils/config_helper.cpp @@ -124,6 +124,7 @@ std::unordered_map ProgramConfig::SETTING_MAP = { {SettingItem::DANMAKU_FILTER_TOP, {"danmaku_filter_top", {}, {}, 1}}, {SettingItem::DANMAKU_FILTER_SCROLL, {"danmaku_filter_scroll", {}, {}, 1}}, {SettingItem::DANMAKU_FILTER_COLOR, {"danmaku_filter_color", {}, {}, 1}}, + {SettingItem::DANMAKU_SMART_MASK, {"danmaku_smart_mask", {}, {}, 1}}, {SettingItem::SEARCH_TV_MODE, {"search_tv_mode", {}, {}, 1}}, /// number @@ -458,6 +459,8 @@ void ProgramConfig::load() { // 初始化弹幕相关内容 DanmakuCore::DANMAKU_ON = getBoolOption(SettingItem::DANMAKU_ON); + DanmakuCore::DANMAKU_SMART_MASK = + getBoolOption(SettingItem::DANMAKU_SMART_MASK); DanmakuCore::DANMAKU_FILTER_SHOW_TOP = getBoolOption(SettingItem::DANMAKU_FILTER_TOP); DanmakuCore::DANMAKU_FILTER_SHOW_BOTTOM = diff --git a/wiliwili/source/utils/string_helper.cpp b/wiliwili/source/utils/string_helper.cpp index e9f3be4a..ddfe48e3 100644 --- a/wiliwili/source/utils/string_helper.cpp +++ b/wiliwili/source/utils/string_helper.cpp @@ -6,9 +6,9 @@ #include #include #include +#include #include "utils/string_helper.hpp" -#include "fmt/format.h" namespace wiliwili { @@ -135,4 +135,42 @@ int base64Decode(const std::string &input, std::string &out) { return 0; } +std::string decompressGzipData(const std::string &compressedData) { + z_stream zs; + memset(&zs, 0, sizeof(zs)); + + if (inflateInit2(&zs, 16 + MAX_WBITS) != Z_OK) { + throw(std::runtime_error("inflateInit failed while decompressing.")); + } + + zs.next_in = (Bytef *)compressedData.data(); + zs.avail_in = compressedData.size(); + + int ret; + char outbuffer[4096]; + std::string decompressedData; + + do { + zs.next_out = reinterpret_cast(outbuffer); + zs.avail_out = sizeof(outbuffer); + + ret = inflate(&zs, 0); + + if (decompressedData.size() < zs.total_out) { + decompressedData.append(outbuffer, + zs.total_out - decompressedData.size()); + } + + } while (ret == Z_OK); + + inflateEnd(&zs); + + if (ret != Z_STREAM_END) { + throw(std::runtime_error(fmt::format( + "Exception during zlib decompression: ({}) {}", ret, zs.msg))); + } + + return decompressedData; +} + }; // namespace wiliwili diff --git a/wiliwili/source/view/danmaku_core.cpp b/wiliwili/source/view/danmaku_core.cpp index 62bc53f6..67b559c3 100644 --- a/wiliwili/source/view/danmaku_core.cpp +++ b/wiliwili/source/view/danmaku_core.cpp @@ -2,13 +2,53 @@ // Created by fang on 2023/1/11. // +#include + #include #include #include +#include #include "view/danmaku_core.hpp" #include "utils/config_helper.hpp" -#include "borealis/core/logger.hpp" +#include "utils/string_helper.hpp" + +// include ntohl / ntohll +#ifdef _WIN32 +#include +#else +#include +#if defined(__linux__) +#include +#elif defined(__FreeBSD__) || defined(__NetBSD__) +#include +#elif defined(__OpenBSD__) +#include +#endif +#endif +#ifdef __SWITCH__ +static inline uint64_t ntohll(uint64_t netlonglong) { + return __builtin_bswap64(netlonglong); +} +#elif defined(__WINRT__) +#elif defined(betoh64) +#define ntohll betoh64 +#elif defined(be64toh) +#define ntohll be64toh +#elif !defined(ntohll) +static inline uint64_t ntohll(uint64_t value) { + if (ntohl(1) == 1) { + // The system is big endian, no conversion is needed + return value; + } else { + // The system is little endian, convert from network byte order (big endian) to host byte order + const uint32_t high_part = ntohl(static_cast(value >> 32)); + const uint32_t low_part = + ntohl(static_cast(value & 0xFFFFFFFFLL)); + return (static_cast(low_part) << 32) | high_part; + } +} +#endif DanmakuItem::DanmakuItem(std::string content, const char *attributes) : msg(std::move(content)) { @@ -51,6 +91,16 @@ DanmakuCore::DanmakuCore() { this->refresh(); } else if (e == MpvEventEnum::RESET) { this->reset(); + } else if (e == MpvEventEnum::VIDEO_SPEED_CHANGE) { + this->setSpeed(MPVCore::instance().getSpeed()); + } + }); + + // 退出前清空遮罩纹理 + brls::Application::getExitDoneEvent()->subscribe([this]() { + if (maskTex != 0) { + nvgDeleteImage(brls::Application::getNVGContext(), maskTex); + maskTex = 0; } }); } @@ -65,9 +115,16 @@ void DanmakuCore::reset() { this->danmakuData.clear(); this->danmakuLoaded = false; danmakuIndex = 0; + maskIndex = 0; + maskSliceIndex = 0; videoSpeed = MPVCore::instance().getSpeed(); lineHeight = DANMAKU_STYLE_FONTSIZE * DANMAKU_STYLE_LINE_HEIGHT * 0.01f; lineNumCurrent = 0; + maskData.clear(); + if (maskTex != 0) { + nvgDeleteImage(brls::Application::getNVGContext(), maskTex); + maskTex = 0; + } danmakuMutex.unlock(); } @@ -92,6 +149,8 @@ void DanmakuCore::addSingleDanmaku(const DanmakuItem &item) { MPVCore::instance().getCustomEvent()->fire("DANMAKU_LOADED", nullptr); } +void DanmakuCore::loadMaskData(const WebMask &data) { maskData = data; } + void DanmakuCore::refresh() { danmakuMutex.lock(); @@ -101,6 +160,10 @@ void DanmakuCore::refresh() { // 将当前屏幕第一条弹幕序号设为0 danmakuIndex = 0; + // 将遮罩序号设为0 + maskSliceIndex = 0; + maskIndex = 0; + // 重置弹幕控制显示的信息 for (auto &i : danmakuData) { i.showing = false; @@ -150,6 +213,8 @@ void DanmakuCore::setSpeed(double speed) { void DanmakuCore::save() { ProgramConfig::instance().setSettingItem(SettingItem::DANMAKU_ON, DANMAKU_ON, false); + ProgramConfig::instance().setSettingItem(SettingItem::DANMAKU_SMART_MASK, + DANMAKU_SMART_MASK, false); ProgramConfig::instance().setSettingItem(SettingItem::DANMAKU_FILTER_TOP, DANMAKU_FILTER_SHOW_TOP, false); ProgramConfig::instance().setSettingItem(SettingItem::DANMAKU_FILTER_BOTTOM, @@ -186,12 +251,17 @@ NVGcolor DanmakuCore::a(NVGcolor color, float alpha) { return color; } +// Uncomment this line to show mask in a more obvious way. +//#define DEBUG_MASK + void DanmakuCore::draw(NVGcontext *vg, float x, float y, float width, - float height, float alpha) { + float height, float alpha) { if (!DanmakuCore::DANMAKU_ON) return; if (!this->danmakuLoaded) return; if (danmakuData.empty()) return; + int64_t currentTime = brls::getCPUTimeUsec(); + double playbackTime = MPVCore::instance().playback_time; float SECOND = 0.12f * DANMAKU_STYLE_SPEED; float CENTER_SECOND = 0.04f * DANMAKU_STYLE_SPEED; @@ -199,6 +269,75 @@ void DanmakuCore::draw(NVGcontext *vg, float x, float y, float width, nvgSave(vg); nvgIntersectScissor(vg, x, y, width, height); + // 设置遮罩 +#ifdef BOREALIS_USE_OPENGL + if (DANMAKU_SMART_MASK && !maskData.sliceData.empty()) { + // 先根据时间选择分片 + while (maskSliceIndex < maskData.sliceData.size()) { + auto &slice = maskData.sliceData[maskSliceIndex + 1]; + if (slice.time > playbackTime * 1000) break; + maskSliceIndex++; + maskIndex = 0; + } + + // 在分片内选择对应时间的svg + // todo: 优化为异步加载 (包括下载和解压分片) + if (maskSliceIndex >= maskData.sliceData.size()) goto skip_mask; + auto &slice = maskData.getSlice(maskSliceIndex); + while (maskIndex < slice.svgData.size()) { + auto &svg = slice.svgData[maskIndex]; + if (svg.showTime > playbackTime * 1000) break; + maskIndex++; + } + if (maskIndex == 0) maskIndex = 1; + + // 设置 svg + if (maskIndex > slice.svgData.size()) goto skip_mask; + auto &svg = slice.svgData[maskIndex - 1]; + // 给图片添加一圈边框(避免图片边沿为透明时自动扩展了透明色导致非视频区域无法显示弹幕) + // 注:返回的 svg 底部固定留有 2像素 透明,不是很清楚具体作用,这里选择绘制一个2像素宽的空心矩形来覆盖 + const std::string border = + R"xml()xml"; + auto maskDocument = lunasvg::Document::loadFromData( + pystring::slice(svg.svg, 0, pystring::rindex(svg.svg, "")) + + border); + if (maskDocument == nullptr) goto skip_mask; + auto bitmap = maskDocument->renderToBitmap(maskDocument->width(), + maskDocument->height()); + uint32_t maskWidth = bitmap.width(); + uint32_t maskHeight = bitmap.height(); + if (maskTex != 0) { + nvgUpdateImage(vg, maskTex, bitmap.data()); + } else { + maskTex = nvgCreateImageRGBA(vg, (int)maskWidth, (int)maskHeight, + NVG_IMAGE_NEAREST, bitmap.data()); + } + + // 设置遮罩 + nvgBeginPath(vg); + float drawHeight = height, drawWidth = width; + float drawX = x, drawY = y; + if (maskWidth * height > maskHeight * width) { + drawHeight = maskHeight * width / maskWidth; + drawY = y + (height - drawHeight) / 2; + } else { + drawWidth = maskWidth * height / maskHeight; + drawX = x + (width - drawWidth) / 2; + } + auto paint = nvgImagePattern(vg, drawX, drawY, drawWidth, drawHeight, 0, + maskTex, alpha); + nvgRect(vg, x, y, width, height); + nvgFillPaint(vg, paint); +#if defined(DEBUG_MASK) + nvgFill(vg); +#else + nvgStencil(vg); +#endif + } +#endif /* BOREALIS_USE_OPENGL */ +skip_mask: + + // 设置基础字体 nvgFontSize(vg, DANMAKU_STYLE_FONTSIZE); nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); nvgFontFaceId(vg, this->danmakuFont); @@ -210,8 +349,6 @@ void DanmakuCore::draw(NVGcontext *vg, float x, float y, float width, } //取出需要的弹幕 - int64_t currentTime = brls::getCPUTimeUsec(); - double playbackTime = MPVCore::instance().playback_time; float bounds[4]; for (size_t j = this->danmakuIndex; j < this->danmakuData.size(); j++) { auto &i = this->danmakuData[j]; @@ -354,5 +491,83 @@ void DanmakuCore::draw(NVGcontext *vg, float x, float y, float width, break; } } + + // 清空遮罩 +#if !defined(DEBUG_MASK) && defined(BOREALIS_USE_OPENGL) + if (maskTex > 0) { + nvgBeginPath(vg); + nvgRect(vg, x, y, width, height); + nvgStencilClear(vg); + } +#endif nvgRestore(vg); +} + +void WebMask::parse(const std::string &text) { + rawData = text; + + // 检查头部 + std::memcpy(&version, rawData.data() + 4, sizeof(int32_t)); + std::memcpy(&check, rawData.data() + 8, sizeof(int32_t)); + std::memcpy(&length, rawData.data() + 12, sizeof(int32_t)); + version = ntohl(version); + check = ntohl(check); + length = ntohl(length); + + // 获取所有分片信息 + sliceData.reserve(length + 1); + int64_t time, offset, currentOffset = 16; + for (size_t i = 0; i < length; i++) { + std::memcpy(&time, rawData.data() + currentOffset, sizeof(int64_t)); + std::memcpy(&offset, rawData.data() + currentOffset + 8, + sizeof(int64_t)); + time = ntohll(time); + offset = ntohll(offset); + sliceData.emplace_back(time, offset, 0); + if (i != 0) sliceData[i - 1].offsetEnd = offset; + if (i == length - 1) sliceData[i].offsetEnd = rawData.size(); + currentOffset += 16; + } + // 再添加一个尾部分片方便计算 + sliceData.emplace_back(-1, -1, -1); +} + +void WebMask::clear() { + this->rawData.clear(); + this->sliceData.clear(); +} + +const MaskSlice &WebMask::getSlice(size_t index) { + auto &slice = sliceData[index]; + if (!slice.svgData.empty()) return slice; + + // 解压分片数据 + std::string data; + try { + data = wiliwili::decompressGzipData(rawData.substr( + slice.offsetStart, slice.offsetEnd - slice.offsetStart)); + } catch (const std::runtime_error &e) { + brls::Logger::error("web mask decompress error: {}", e.what()); + return slice; + } + + // 获取svg数据 + size_t sliceOffset = 0; + uint32_t sliceLength, sliceTime; + while (sliceOffset < data.size()) { + std::memcpy(&sliceLength, data.data() + sliceOffset, sizeof(int32_t)); + std::memcpy(&sliceTime, data.data() + sliceOffset + 8, sizeof(int32_t)); + sliceLength = ntohl(sliceLength); + sliceTime = ntohl(sliceTime); + sliceOffset += 12; + auto base64 = pystring::replace( + pystring::split(data.substr(sliceOffset, sliceLength), ",", 1)[1], + "\n", ""); + std::string svg; + wiliwili::base64Decode(base64, svg); + sliceOffset += sliceLength; + + slice.svgData.emplace_back(svg, sliceTime); + } + return slice; } \ No newline at end of file diff --git a/wiliwili/source/view/mpv_core.cpp b/wiliwili/source/view/mpv_core.cpp index f06f96f9..87d7d357 100644 --- a/wiliwili/source/view/mpv_core.cpp +++ b/wiliwili/source/view/mpv_core.cpp @@ -443,8 +443,8 @@ void MPVCore::initializeVideo() { // create texture glGenTextures(1, &this->media_texture); glBindTexture(GL_TEXTURE_2D, this->media_texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, (int)brls::Application::windowWidth, - (int)brls::Application::windowHeight, 0, GL_RGB, + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (int)brls::Application::windowWidth, + (int)brls::Application::windowHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); @@ -579,7 +579,7 @@ void MPVCore::setFrameSize(brls::Rect r) { if (drawWidth == 0 || drawHeight == 0) return; brls::Logger::debug("MPVCore::setFrameSize: {}/{}", drawWidth, drawHeight); glBindTexture(GL_TEXTURE_2D, this->media_texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, drawWidth, drawHeight, 0, GL_RGB, + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, drawWidth, drawHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); this->mpv_fbo.w = drawWidth; this->mpv_fbo.h = drawHeight; diff --git a/wiliwili/source/view/video_view.cpp b/wiliwili/source/view/video_view.cpp index 0b892174..e712eeee 100644 --- a/wiliwili/source/view/video_view.cpp +++ b/wiliwili/source/view/video_view.cpp @@ -670,7 +670,7 @@ void VideoView::drawHighlightProgress(NVGcontext* vg, float x, float y, float width, float alpha) { if (highlight_data.size() <= 1) return; nvgBeginPath(vg); - nvgFillColor(vg, nvgRGBA(255, 255, 255, (unsigned char)(128 * alpha))); + nvgFillColor(vg, nvgRGBAf(1.0f, 1.0f, 1.0f, 0.5f * alpha)); float baseY = y; float dX = width / ((float)highlight_data.size() - 1); float halfDx = dX / 2; @@ -685,7 +685,12 @@ void VideoView::drawHighlightProgress(NVGcontext* vg, float x, float y, pointX += dX; float pointY = baseY - 12 - item * 48; float cx = lastX + halfDx; - nvgBezierTo(vg, cx, lastY, cx, pointY, pointX, pointY); + if (fabs(lastY - pointY) < 3) { + // 相差太小,直接绘制直线 + nvgLineTo(vg, pointX, pointY); + } else { + nvgBezierTo(vg, cx, lastY, cx, pointY, pointX, pointY); + } lastX = pointX; lastY = pointY; }