第三方哔哩哔哩安卓客户端 | 支持平板、TV与车机

1 阅读5分钟

第三方哔哩哔哩安卓客户端

一个专为平板、TV及车机设备优化的第三方哔哩哔哩安卓客户端,支持触摸与遥控操作,兼容安卓5.0以上系统。

功能特性

  • 多设备适配:同时支持触摸屏(平板、手机)与遥控器(TV、车机)操作
  • 完整功能导航:侧边栏集成搜索、推荐、分类、动态、直播、我的个人中心
  • 扫码登录:支持B站扫码登录,安全便捷
  • 强大视频播放:基于Media3(ExoPlayer)内核,支持分辨率切换、编码选择、播放倍速、字幕显示与弹幕系统
  • 丰富的播放设置:独立的设置页面,可调整播放与弹幕偏好
  • 追番追踪:专门的追番页面,不错过任何更新

安装指南

环境要求

  • JDK 17
  • Android SDK (compileSdk 36)
  • 设备系统:Android 5.0+

构建步骤

  1. 克隆仓库

    git clone https://github.com/your-repo/blbl-android.git
    cd blbl-android
    
  2. 调试包构建

    ./gradlew assembleDebug
    
  3. 发布包构建(已开启R8混淆与资源压缩)

    ./gradlew assembleRelease
    
  4. 自定义版本号构建(适用于本地或CI)

    ./gradlew assembleRelease -PversionName=0.1.1 -PversionCode=2
    

临时更新方案:当前版本内置了国内可直接访问的直链,便于测试阶段覆盖更新。稳定后将移除该功能。如需纯净版本,请从Release页面下载Action编译的安装包。

使用说明

基础操作

  • 侧边栏导航:从屏幕左侧滑动或点击左上角图标打开导航菜单
  • 扫码登录:在"我的"页面点击登录,使用B站App扫码完成授权
  • 视频播放:点击任意视频卡片进入播放页面,支持:
    • 分辨率/编码切换
    • 倍速播放(0.5x - 2.0x)
    • 字幕开关与选择
    • 弹幕显示/隐藏与设置

典型使用场景

场景一:平板追番

  1. 登录后进入"追番"页面
  2. 浏览追更列表,点击剧集卡片
  3. 在播放页调整清晰度与倍速,开启弹幕互动

场景二:TV遥控观看

  1. 使用遥控器方向键选择推荐视频
  2. 确认键进入播放,菜单键调出播放设置
  3. 按返回键返回上一级

核心模块概览

模块说明
推荐页个性化视频流推送
分类页按分区浏览内容
动态页关注UP主的最新动态
直播页正在进行的直播列表
我的页个人信息、历史记录与设置
搜索页关键词搜索视频/UP主

核心代码

1. 视频播放器初始化(基于Media3 ExoPlayer)

// PlayerManager.kt - 播放器核心初始化片段
class PlayerManager(private val context: Context) {
    private var exoPlayer: ExoPlayer? = null
    
    fun initPlayer(rendererView: RendererView): ExoPlayer {
        val trackSelector = DefaultTrackSelector(context).apply {
            setParameters(buildUponParameters {
                setMaxVideoSize(1920, 1080)
                setPreferredVideoMimeType(MimeTypes.VIDEO_H264)
            })
        }
        
        exoPlayer = ExoPlayer.Builder(context)
            .setTrackSelector(trackSelector)
            .setLoadControl(DefaultLoadControl())
            .setBandwidthMeter(DefaultBandwidthMeter.Builder(context).build())
            .build()
        
        exoPlayer?.apply {
            setVideoSurfaceView(rendererView.surfaceView)
            addListener(playerEventListener)
        }
        
        return exoPlayer!!
    }
    
    fun preparePlayback(url: String, headers: Map<String, String>) {
        val mediaItem = MediaItem.Builder()
            .setUri(url)
            .setCustomCacheKey(url)
            .setMimeType(MimeTypes.APPLICATION_M3U8)
            .build()
        
        exoPlayer?.setMediaItem(mediaItem)
        exoPlayer?.prepare()
        exoPlayer?.playWhenReady = true
    }
}

2. 弹幕引擎渲染逻辑

// DanmakuEngine.kt - 弹幕解析与渲染核心
class DanmakuEngine(private val surfaceView: SurfaceView) {
    private var danmakuContext: DanmakuContext? = null
    private var danmakuParser: BaseDanmakuParser? = null
    
    fun init() {
        danmakuContext = DanmakuContext.create()
        danmakuParser = object : BaseDanmakuParser() {
            override fun parse(): DanmakuList? {
                // 解析B站protobuf格式弹幕数据
                val danmakuList = DanmakuList()
                // 弹幕数据转换逻辑...
                return danmakuList
            }
        }
        
        DanmakuView(surfaceView.context).apply {
            setCallback(object : DanmakuView.Callback {
                override fun prepared() {
                    start()
                }
                override fun updateTimer(interval: Long) {}
                override fun rendererFailed(exception: Throwable) {}
            })
            prepare(danmakuParser, danmakuContext)
        }
    }
    
    fun addDanmaku(content: String, color: Int, type: Int) {
        val danmaku = danmakuContext?.mDanmakuFactory?.createDanmaku(type)
        danmaku?.apply {
            text = content
            textColor = color
            setTime(System.currentTimeMillis())
            timing = currentPosition
        }
        danmakuContext?.getDanmakuView()?.addDanmaku(danmaku)
    }
}

3. 网络请求与API封装(OkHttp + Protobuf)

// ApiService.kt - B站API请求封装示例
class BiliApiService {
    private val client = OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .addInterceptor(AuthInterceptor())
        .addInterceptor(GzipInterceptor())
        .build()
    
    suspend fun getRecommendVideos(page: Int): List<VideoItem> {
        val request = Request.Builder()
            .url("https://api.bilibili.com/x/web-interface/index/top/rcmd")
            .addHeader("User-Agent", USER_AGENT)
            .addHeader("Referer", "https://www.bilibili.com")
            .addQueryParameter("fresh_type", "4")
            .addQueryParameter("version", "1")
            .addQueryParameter("pn", page.toString())
            .build()
        
        val response = client.newCall(request).await()
        val protobufData = response.body?.bytes() ?: return emptyList()
        
        // 解析protobuf-lite格式数据
        return BiliProto.RecommendResponse.parseFrom(protobufData).videoListList
            .map { it.toVideoItem() }
    }
    
    private class AuthInterceptor : Interceptor {
        override fun intercept(chain: Interceptor.Chain): Response {
            val original = chain.request()
            val request = original.newBuilder()
                .addHeader("Cookie", getStoredCookie())
                .addHeader("Authorization", "Bearer ${getAccessToken()}")
                .build()
            return chain.proceed(request)
        }
    }
}

4. 侧边栏导航与Fragment容器

// MainActivity.kt - 导航抽屉与Fragment切换
class MainActivity : AppCompatActivity() {
    private lateinit var drawerLayout: DrawerLayout
    private lateinit var navView: NavigationView
    private lateinit var fragmentContainer: FrameLayout
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        drawerLayout = findViewById(R.id.drawer_layout)
        navView = findViewById(R.id.nav_view)
        fragmentContainer = findViewById(R.id.fragment_container)
        
        setupNavigation()
        
        // 默认加载推荐页
        if (savedInstanceState == null) {
            supportFragmentManager.beginTransaction()
                .replace(R.id.fragment_container, RecommendFragment())
                .commit()
        }
    }
    
    private fun setupNavigation() {
        navView.setNavigationItemSelectedListener { menuItem ->
            val fragment = when (menuItem.itemId) {
                R.id.nav_recommend -> RecommendFragment()
                R.id.nav_category -> CategoryFragment()
                R.id.nav_dynamic -> DynamicFragment()
                R.id.nav_live -> LiveFragment()
                R.id.nav_profile -> ProfileFragment()
                R.id.nav_search -> SearchFragment()
                R.id.nav_follow -> FollowAnimeFragment()
                else -> null
            }
            
            fragment?.let {
                supportFragmentManager.beginTransaction()
                    .replace(R.id.fragment_container, it)
                    .commit()
                drawerLayout.closeDrawer(GravityCompat.START)
            }
            true
        }
    }
}

技术栈

  • 语言与框架:Kotlin + AndroidX + ViewBinding
  • 视频播放:Media3 (ExoPlayer)
  • 网络请求:OkHttp + Protobuf-lite
  • UI组件:Material Design Components / RecyclerView / ViewPager2
  • 构建工具:Gradle + R8混淆 + 资源压缩

GitHub Actions

项目包含两套手动触发工作流:

  • Android Debug:手动输入version_name触发调试包构建
  • Android Release:手动输入version_name,需要配置签名Secrets

需要配置的仓库Secrets:

  • RELEASE_KEYSTORE_BASE64
  • RELEASE_STORE_PASSWORD
  • RELEASE_KEY_ALIAS
  • RELEASE_KEY_PASSWORD

待办事项

  • 完善遥控器操作逻辑
  • 统一样式大小计算规则
  • 移除测试用内置直链更新方案

致谢

  • B站API收集整理
  • BBLL - 优秀的页面设计和操作逻辑
  • PiliPlus - 部分关键功能参考
  • 开源第三方B站客户端生态
  • 群友们的详细测试与反馈

免责声明

不得利用本项目进行任何非法活动,不得干扰B站的正常运营,不得传播恶意软件或病毒。

为降低法律风险,请遵守以下规范:

  1. 🚫 禁止在官方平台(b站)及官方账号区域(如b站微博评论区)宣传本项目
  2. 🚫 禁止在微信公众号平台宣传本项目
  3. 🚫 禁止利用本项目牟利,本项目无任何盈利行为,第三方盈利与本项目无关

代码由AI生成,如有问题请联系 OpenAI 😤 GqIcyjPXfClN0ovBixvyWHpB0K/qCSlwybNZDTCBJwA=