利用短代码在hugo博客中加入bilibili播放器插件

王叨叨在 开源Bilibili播放器插件 一文中,分享了一个Typecho插件,用于将Bilibili默认播放器替换为HTML5移动端播放器,并提供更多自定义选项。

基于他的源代码,考虑将其移植到Hugo中,并通过短代码来实现bilibili视频的调用。

短代码是Hugo的一项强大功能,允许我们在Markdown中嵌入HTML片段,实现复杂的布局和功能。

王叨叨的Typecho插件主要通过以下方式工作:

  1. 通过正则表达式匹配和替换Bilibili播放器iframe
  2. 提供丰富的配置选项(宽度、高度、自动播放、弹幕开关等)
  3. 替换默认播放器为HTML5移动端播放器,提升用户体验

在上述的基础上,借助AI实现了一定程度的高度自适应和自定义封面功能,为什么说是一定程度上的高度自适应,其实是新加入了ratio参数,即明确了视频宽高比,再根据视频宽高比,使用 padding-bottom 技巧实现真正的宽高比自适应。

示例

具体实现

创建短代码文件

在Hugo项目的 layouts/shortcodes/ 目录下创建 bilibili.html 文件:

{{- $id := .Get "id" -}}
{{- $page := .Get "page" | default "1" -}}
{{- $width := .Get "width" | default "100%" -}}
{{- $height := .Get "height" | default "auto" -}}
{{- $ratio := .Get "ratio" | default "16x9" -}}
{{- $cover := .Get "cover" | default "" -}}
{{- $coverPosition := .Get "coverPosition" | default "center" -}}
{{- $autoplay := .Get "autoplay" | default "0" -}}
{{- $danmaku := .Get "danmaku" | default "1" -}}
{{- $muted := .Get "muted" | default "0" -}}
{{- $hasMuteButton := .Get "hasMuteButton" | default "0" -}}
{{- $hideCoverInfo := .Get "hideCoverInfo" | default "0" -}}
{{- $hideDanmakuButton := .Get "hideDanmakuButton" | default "0" -}}
{{- $noFullScreenButton := .Get "noFullScreenButton" | default "0" -}}
{{- $fjw := .Get "fjw" | default "1" -}}

{{- $queryParams := dict "page" $page -}}
{{- if ne $autoplay "0" }}{{ $queryParams = merge $queryParams (dict "autoplay" $autoplay) }}{{ end -}}
{{- if ne $danmaku "0" }}{{ $queryParams = merge $queryParams (dict "danmaku" $danmaku) }}{{ end -}}
{{- if ne $muted "0" }}{{ $queryParams = merge $queryParams (dict "muted" $muted) }}{{ end -}}
{{- if ne $hasMuteButton "0" }}{{ $queryParams = merge $queryParams (dict "hasMuteButton" $hasMuteButton) }}{{ end -}}
{{- if ne $hideCoverInfo "0" }}{{ $queryParams = merge $queryParams (dict "hideCoverInfo" $hideCoverInfo) }}{{ end -}}
{{- if ne $hideDanmakuButton "0" }}{{ $queryParams = merge $queryParams (dict "hideDanmakuButton" $hideDanmakuButton) }}{{ end -}}
{{- if ne $noFullScreenButton "0" }}{{ $queryParams = merge $queryParams (dict "noFullScreenButton" $noFullScreenButton) }}{{ end -}}
{{- if ne $fjw "0" }}{{ $queryParams = merge $queryParams (dict "fjw" $fjw) }}{{ end -}}

{{- $aspectRatio := "56.25%" -}}
{{- if eq $ratio "4x3" }}{{ $aspectRatio = "75%" }}{{ end -}}
{{- if eq $ratio "1x1" }}{{ $aspectRatio = "100%" }}{{ end -}}
{{- if eq $ratio "21x9" }}{{ $aspectRatio = "42.86%" }}{{ end -}}

{{- $playerID := printf "bilibili-player-%s" (md5 (printf "%s-%s" $id $page)) -}}

<style>
.bilibili-player-wrapper {
    position: relative;
    width: {{ $width }};
    {{ if eq $height "auto" }}
    height: 0;
    padding-bottom: {{ $aspectRatio }};
    {{ else }}
    height: {{ $height }};
    {{ end }}
    margin: 1rem 0;
    border-radius: 8px;
    overflow: hidden;
    background-color: #f5f5f5;
}

.bilibili-player-wrapper iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border: none;
    border-radius: 8px;
}

.bilibili-custom-cover {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-size: cover;
    background-position: {{ $coverPosition }};
    background-repeat: no-repeat;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 8px;
    z-index: 10;
    transition: opacity 0.3s ease;
}

.bilibili-custom-cover:hover .bilibili-play-button {
    transform: scale(1.1);
    background-color: #00a1d6;
}

.bilibili-play-button {
    width: 70px;
    height: 70px;
    background-color: rgba(0, 161, 214, 0.9);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
    font-size: 24px;
    transition: all 0.3s ease;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

.bilibili-play-button::after {
    content: "▶";
    margin-left: 4px;
}

.bilibili-player-loading {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: rgba(0, 0, 0, 0.7);
    color: white;
    font-size: 16px;
    z-index: 5;
    border-radius: 8px;
}

.bilibili-player-loading.hidden {
    display: none;
}

@media (max-width: 768px) {
    .bilibili-play-button {
        width: 60px;
        height: 60px;
        font-size: 20px;
    }
}
</style>

{{- if $id -}}
<div class="bilibili-player-wrapper" id="{{ $playerID }}">
    {{- if $cover -}}
    <div class="bilibili-custom-cover" style="background-image: url('{{ $cover }}');" onclick="loadBilibiliPlayer('{{ $playerID }}')">
        <div class="bilibili-play-button"></div>
    </div>
    {{- end -}}
    
    <div class="bilibili-player-loading hidden">视频加载中...</div>
    
    {{- if hasPrefix $id "BV" -}}
    <iframe 
        {{ if $cover }}data-src{{ else }}src{{ end }}="//www.bilibili.com/blackboard/html5mobileplayer.html?bvid={{ $id }}{{ with $queryParams }}{{ if gt (len .) 0 }}&{{ . | querify | safeURL }}{{ end }}{{ end }}"
        scrolling="no"
        frameborder="no"
        framespacing="0"
        allowfullscreen="true"
        loading="lazy"
        {{ if $cover }}style="display:none;"{{ end }}>
    </iframe>
    {{- else if hasPrefix $id "av" -}}
    <iframe 
        {{ if $cover }}data-src{{ else }}src{{ end }}="//www.bilibili.com/blackboard/html5mobileplayer.html?aid={{ strings.TrimPrefix "av" $id }}{{ with $queryParams }}{{ if gt (len .) 0 }}&{{ . | querify | safeURL }}{{ end }}{{ end }}"
        scrolling="no"
        frameborder="no"
        framespacing="0"
        allowfullscreen="true"
        loading="lazy"
        {{ if $cover }}style="display:none;"{{ end }}>
    </iframe>
    {{- else -}}
    {{- errorf "Invalid Bilibili video ID: %s. Must start with 'BV' or 'av'" $id -}}
    {{- end -}}
</div>

<script>
function loadBilibiliPlayer(playerId) {
    const playerWrapper = document.getElementById(playerId);
    const cover = playerWrapper.querySelector('.bilibili-custom-cover');
    const iframe = playerWrapper.querySelector('iframe');
    const loading = playerWrapper.querySelector('.bilibili-player-loading');
    
    if (iframe && iframe.hasAttribute('data-src')) {
        // 显示加载提示
        loading.classList.remove('hidden');
        
        // 设置iframe的src属性
        iframe.src = iframe.getAttribute('data-src');
        
        // 显示iframe
        iframe.style.display = 'block';
        
        // 隐藏封面
        cover.style.opacity = '0';
        
        // 等待iframe加载完成后隐藏加载提示
        iframe.onload = function() {
            loading.classList.add('hidden');
            setTimeout(function() {
                cover.style.display = 'none';
            }, 300);
        };
    }
}

// 如果没有自定义封面,直接显示播放器
document.addEventListener('DOMContentLoaded', function() {
    const players = document.querySelectorAll('.bilibili-player-wrapper');
    players.forEach(function(player) {
        const cover = player.querySelector('.bilibili-custom-cover');
        const iframe = player.querySelector('iframe');
        
        if (!cover && iframe && iframe.hasAttribute('data-src')) {
            iframe.src = iframe.getAttribute('data-src');
            iframe.style.display = 'block';
        }
    });
});
</script>
{{- else -}}
{{- errorf "Missing required parameter 'id' for bilibili shortcode" -}}
{{- end -}}

使用方法

基本用法

在Hugo文章的Markdown内容中,你可以通过以下方式使用短代码:

// 基本用法(BV号)
{{< bilibili id="BV1CG1LBbENh" >}}

// 基本用法(av号)
{{< bilibili id="av810872" >}}

// 基本用法(带自定义封面)
{{< bilibili id="BV1CG1LBbENh" cover="images/cover.jpg" >}}

// 带分P的视频
{{< bilibili id="BV1CG1LBbENh" page="2" >}}

// 自定义宽高比
{{< bilibili id="BV1CG1LBbENh" ratio="4x3" >}}

// 固定宽度(仍然自适应高度)
{{< bilibili id="BV1CG1LBbENh" width="800px" >}}

// 强制固定高度(不推荐)
{{< bilibili id="BV1CG1LBbENh" height="400px" >}}

// 开启自动播放和静音
{{< bilibili id="BV1CG1LBbENh" autoplay="1" muted="1" >}}

// 关闭弹幕
{{< bilibili id="BV1CG1LBbENh" danmaku="0" >}}

// 自定义封面和位置
{{< bilibili 
    id="BV1CG1LBbENh" 
    cover="images/cover.jpg"
    coverPosition="top"
>}}

// 完整参数示例
{{< bilibili 
    id="BV1GJ411x7h7" 
    page="1"
    width="100%"
    ratio="16x9"
    cover="images/cover.jpg"
    coverPosition="center"
    autoplay="0"
    danmaku="1"
    muted="0"
    hasMuteButton="0"
    hideCoverInfo="0"
    hideDanmakuButton="0"
    noFullScreenButton="0"
    fjw="1"
>}}

参数说明

参数说明默认值使用方法
idB站视频BV号(或AV号)必填例如:BV1CG1LBbENh
page视频分P1
width播放器宽度100%例如:100%, 800px
ratio视频宽高比16x916x9, 4x3, 1x1, 21x9
cover自定义封面图片URL任何有效的图片URL
coverPosition封面图片定位centercenter, top, bottom, left, right
height播放器高度auto该参数一般无需明确,也可强制定义高度值
autoplay是否自动播放01: 开启, 0: 关闭
danmaku默认弹幕开关11: 开启, 0: 关闭
muted是否静音播放01: 开启, 0: 关闭
hasMuteButton一键静音按钮是否显示01: 开启, 0: 关闭
hideCoverInfo视频封面下方信息显示01: 隐藏, 0: 显示
hideDanmakuButton是否隐藏弹幕按钮01: 隐藏, 0: 显示
noFullScreenButton是否隐藏全屏按钮01: 隐藏, 0: 显示
fjw是否开始记忆播放11: 开启, 0: 关闭

扩展功能

如果你需要更多自定义功能,可以参考B站官方提供的参数进行扩展:

<!-- 在短代码中添加更多B站播放器参数 -->
{{ $highQuality := .Get "highQuality" | default "1" }}
{{ $asWide := .Get "asWide" | default "1" }}

<!-- 然后在iframe的src中添加这些参数 -->
&high_quality={{ $highQuality }}&as_wide={{ $asWide }}
评论