-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.html
181 lines (163 loc) · 34.4 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
<!doctype html>
<html lang="zh"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"><meta><title>Foundation chronicle</title><link rel="manifest" href="/manifest.json"><meta name="application-name" content="Foundation chronicle"><meta name="msapplication-TileImage" content="/favicon.jpg"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-title" content="Foundation chronicle"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="description" content="A blog mainly focused on android &#x2F; FOSS software development."><meta property="og:type" content="blog"><meta property="og:title" content="Foundation chronicle"><meta property="og:url" content="https://blog.akanework.org/"><meta property="og:site_name" content="Foundation chronicle"><meta property="og:description" content="A blog mainly focused on android &#x2F; FOSS software development."><meta property="og:locale" content="zh_CN"><meta property="og:image" content="https://blog.akanework.org/img/og_image.png"><meta property="article:author" content="Shinjo Akane"><meta property="article:tag" content="AkaneTan, AkaneFoundation"><meta property="twitter:card" content="summary"><meta property="twitter:image:src" content="https://blog.akanework.org/img/og_image.png"><script type="application/ld+json">{"@context":"https://schema.org","@type":"BlogPosting","mainEntityOfPage":{"@type":"WebPage","@id":"https://blog.akanework.org"},"headline":"Foundation chronicle","image":["https://blog.akanework.org/img/og_image.png"],"author":{"@type":"Person","name":"Shinjo Akane"},"publisher":{"@type":"Organization","name":"Foundation chronicle","logo":{"@type":"ImageObject","url":{"text":"Foundation Chronicle"}}},"description":"A blog mainly focused on android / FOSS software development."}</script><link rel="icon" href="/favicon.jpg"><link rel="stylesheet" href="https://use.fontawesome.com/releases/v6.0.0/css/all.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/styles/atom-one-light.css"><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;600&family=Source+Code+Pro"><link rel="stylesheet" href="/css/default.css"><style>body>.footer,body>.navbar,body>.section{opacity:0}</style><!--!--><!--!--><!--!--><!--!--><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/build/cookieconsent.min.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/lightgallery.min.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/justifiedGallery.min.css"><!--!--><!--!--><style>.pace{-webkit-pointer-events:none;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.pace-inactive{display:none}.pace .pace-progress{background:#3273dc;position:fixed;z-index:2000;top:0;right:100%;width:100%;height:2px}</style><script src="https://cdn.jsdelivr.net/npm/[email protected]/pace.min.js"></script><!--!--><!--!--><!-- hexo injector head_end start --><script>
(function () {
function switchTab() {
if (!location.hash) {
return;
}
const id = '#' + CSS.escape(location.hash.substring(1));
const $tabMenu = document.querySelector(`.tabs a[href="${id}"]`);
if (!$tabMenu) {
return;
}
const $tabMenuContainer = $tabMenu.parentElement.parentElement;
Array.from($tabMenuContainer.children).forEach($menu => $menu.classList.remove('is-active'));
Array.from($tabMenuContainer.querySelectorAll('a'))
.map($menu => document.getElementById($menu.getAttribute("href").substring(1)))
.forEach($content => $content.classList.add('is-hidden'));
if ($tabMenu) {
$tabMenu.parentElement.classList.add('is-active');
}
const $activeTab = document.querySelector(id);
if ($activeTab) {
$activeTab.classList.remove('is-hidden');
}
}
switchTab();
window.addEventListener('hashchange', switchTab, false);
})();
</script><!-- hexo injector head_end end --><meta name="generator" content="Hexo 6.3.0"></head><body class="is-2-column"><nav class="navbar navbar-main"><div class="container navbar-container"><div class="navbar-brand justify-content-center"><a class="navbar-item navbar-logo" href="/">Foundation Chronicle</a></div><div class="navbar-menu"><div class="navbar-start"><a class="navbar-item is-active" href="/">Home</a><a class="navbar-item" href="/archives">Archives</a><a class="navbar-item" href="/categories">Categories</a><a class="navbar-item" href="/tags">Tags</a></div><div class="navbar-end"><a class="navbar-item" target="_blank" rel="noopener" title="Download on GitHub" href="https://github.com/ppoffice/hexo-theme-icarus"><i class="fab fa-github"></i></a><a class="navbar-item search" title="搜索" href="javascript:;"><i class="fas fa-search"></i></a></div></div></div></nav><section class="section"><div class="container"><div class="columns"><div class="column order-2 column-main is-8-tablet is-8-desktop is-8-widescreen"><div class="card"><article class="card-content article" role="article"><div class="article-meta is-size-7 is-uppercase level is-mobile"><div class="level-left"><span class="level-item"><time dateTime="2023-09-24T16:15:50.000Z" title="2023/9/25 00:15:50">2023-09-25</time>发表</span><span class="level-item"><time dateTime="2023-09-25T11:25:29.275Z" title="2023/9/25 19:25:29">2023-09-25</time>更新</span><span class="level-item"><a class="link-muted" href="/categories/Technical/">Technical</a></span><span class="level-item">11 分钟读完 (大约1703个字)</span></div></div><p class="title is-3 is-size-4-mobile"><a class="link-muted" href="/2023/09/25/%E6%B5%85%E8%B0%88media3%E7%9A%84%E5%AE%9E%E7%8E%B0/">浅谈 Media3 (一)</a></p><div class="content"><h2 id="一、Media3-的前世今生"><a href="#一、Media3-的前世今生" class="headerlink" title="一、Media3 的前世今生"></a>一、Media3 的前世今生</h2><blockquote><p>Jetpack Media3 是媒体库的新家,为 Android 应用程序带来丰富的音频和视觉体验。 Media3 提供了一个具有强大的定制能力、可靠性极高和基于设备优化的简单架构,以消除碎片带来的复杂性。</p>
<footer><strong>Android Developers 文档</strong></footer></blockquote>
<p>今年谷歌为 Media3, 也就是安卓最新的媒体播放实现库推出了第一个稳定版本。Media3 比起先前的 Media2, Exoplayer 有着极高的可定制性。</p>
<p>在 Media3 这样的库发布之前之前写一个播放器是非常困难的@w@ 笔者之前的项目使用的是远古库 MediaPlayer, 不仅要手动处理播放清单、随机等逻辑, 和系统交互的 MediaSession 通知也需要自己构建。更不用提 MediaPlayer 还会因为各种奇奇怪怪的理由罢工 2333</p>
<p><a target="_blank" rel="noopener" href="https://github.com/AkaneTan/Symphonica/blob/beta/app/src/main/java/org/akanework/symphonica/logic/service/SymphonicaPlayerService.kt">示例代码</a></p>
<p>感兴趣的读者可以点开上面的链接看看笔者之前的实现。到这个项目 Archive 为止,实际上部分关于媒体服务的 Impl 还没有写完 (蓝牙控制,播放恢复等)</p>
<p>Media3 为了规范播放器的架构将 Playback Notification / Resumption 的实现都包装在了一起, 让实现一个高效且标准的播放器非常容易。</p>
<h2 id="二、Media3-的美好愿景"><a href="#二、Media3-的美好愿景" class="headerlink" title="二、Media3 的美好愿景"></a>二、Media3 的美好愿景</h2><p>首先让我们来简单了解一下现在市面上大部分音乐/视频播放器所使用的软件结构:</p>
<pre class="line-numbers language-none"><code class="language-none">服务层 活动(Activity)层
Player <-> MediaSession <--------> 媒体控制器 <-> UI<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre>
<p>如上面的简例所示,服务层包含了 Player 本体、与 Android 系统 / 活动层 对接的 MediaSession。而活动层包含了与服务对接的媒体控制器还有呈现给用户看的UI界面。</p>
<p>但有些没有实战过的读者可能会问了:<br><strong>这不是很简单吗???</strong></p>
<p>实际上上面的示意图省去了很多部分,比如 MediaSession 和 Player 之间对接的控制器,媒体控制器还有自己对于播放的接口,需要UI去实现。这导致了一个非常大的问题,结构太过于复杂。代码的复杂性只会导致更多的Bug。</p>
<p>甚至在实现控制器的时候还有多个库可以使用 (ExoPlayer, Media2),这往往会给开发者造成困惑。</p>
<p>Media3 的处理是这样的:</p>
<pre class="line-numbers language-none"><code class="language-none">服务层 活动(Activity)层
ExoPlayer <-> MediaSession <--------> 媒体控制器 <-> UI
^ ^
| |
----------------------> Player <------<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre>
<p>在 Media3 的理想结构下, 四个部件都不用做连接器处理。它们都直接实现通用播放器的接口。这不仅省去了写一大堆连接器的麻烦, 而且让代码更加易于维护。可能之前我们用 MediaPlayer 实例的时候需要自己处理 Activity 与 Service 之间的通信, 但在 Media3 中, 这些都已经被预制的通用播放器解决了。</p>
<p>你还会发现, 当你采用了 Media3 的架构来写播放器的时候, UI 层和服务层是完全分离的。这意味着<strong>更少的错误</strong>和<strong>更好的独立性</strong>。对于一个存活的 Service, 可以有多个播放器界面存在。</p>
<p>同时, Media3 还整合了 ExoPlayer2 作为它可选的播放器实现。</p>
<p>在 Android 13 中, 安卓推送了一项新功能: Playback Resumption (也就是前文所说的播放恢复)。当使用 Playback Resumption 的时候, 应用程序生成的 Media Control 通知 应该常驻于快速设置的面板上, 这样即使播放器根本不在运行, 使用 Media Control 也能迅速开始播放退出时的播放清单。</p>
<p>一些比较知名的播放器,比如自称安卓第一的 <code>Retro Music Player</code>, 还有 <code>Oto Music</code> 都没有把 Playback Resumption 做好。事实上,根据笔者的观察, 市面上除了笔者自己的 <code>Gramophone</code> 和 <code>Spotify</code> 之外似乎没有任何一个播放器正确的植入了 Playback Resumption。这导致 Playback Resumption 这一功能就等于作废, 停驻的 Media Control 通知并不会迅速恢复播放, 只会 **”假死”**。</p>
<p>幸运的是, Media3 发现了这一现状, 并且提供了相应的接口方便的供开发者来实现 Playback Resumption。</p>
<h2 id="三、实现"><a href="#三、实现" class="headerlink" title="三、实现"></a>三、实现</h2><p><strong>服务端</strong></p>
<pre class="line-numbers language-kotlin" data-language="kotlin"><code class="language-kotlin"><span class="token keyword">import</span> androidx<span class="token punctuation">.</span>media3<span class="token punctuation">.</span>common<span class="token punctuation">.</span>AudioAttributes
<span class="token keyword">import</span> androidx<span class="token punctuation">.</span>media3<span class="token punctuation">.</span>common<span class="token punctuation">.</span>C
<span class="token keyword">import</span> androidx<span class="token punctuation">.</span>media3<span class="token punctuation">.</span>common<span class="token punctuation">.</span>Player
<span class="token keyword">import</span> androidx<span class="token punctuation">.</span>media3<span class="token punctuation">.</span>exoplayer<span class="token punctuation">.</span>ExoPlayer
<span class="token keyword">import</span> androidx<span class="token punctuation">.</span>media3<span class="token punctuation">.</span>session<span class="token punctuation">.</span>MediaSession
<span class="token keyword">import</span> androidx<span class="token punctuation">.</span>media3<span class="token punctuation">.</span>session<span class="token punctuation">.</span>MediaSessionService
<span class="token keyword">class</span> PlaybackService <span class="token operator">:</span> <span class="token function">MediaSessionService</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">private</span> <span class="token keyword">var</span> mediaSession<span class="token operator">:</span> MediaSession<span class="token operator">?</span> <span class="token operator">=</span> <span class="token keyword">null</span>
<span class="token keyword">override</span> <span class="token keyword">fun</span> <span class="token function">onCreate</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">val</span> player <span class="token operator">=</span> ExoPlayer<span class="token punctuation">.</span><span class="token function">Builder</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">val</span> audioAttributes<span class="token operator">:</span> AudioAttributes <span class="token operator">=</span> AudioAttributes<span class="token punctuation">.</span><span class="token function">Builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">setUsage</span><span class="token punctuation">(</span>C<span class="token punctuation">.</span>USAGE_MEDIA<span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">setContentType</span><span class="token punctuation">(</span>C<span class="token punctuation">.</span>AUDIO_CONTENT_TYPE_MUSIC<span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
player<span class="token punctuation">.</span><span class="token function">setAudioAttributes</span><span class="token punctuation">(</span>audioAttributes<span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onCreate</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">override</span> <span class="token keyword">fun</span> <span class="token function">onDestroy</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
mediaSession<span class="token operator">?</span><span class="token punctuation">.</span><span class="token function">run</span> <span class="token punctuation">{</span>
player<span class="token punctuation">.</span><span class="token function">release</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token function">release</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
mediaSession <span class="token operator">=</span> <span class="token keyword">null</span>
<span class="token punctuation">}</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onDestroy</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">override</span> <span class="token keyword">fun</span> <span class="token function">onGetSession</span><span class="token punctuation">(</span>controllerInfo<span class="token operator">:</span> MediaSession<span class="token punctuation">.</span>ControllerInfo<span class="token punctuation">)</span><span class="token operator">:</span> MediaSession<span class="token operator">?</span> <span class="token operator">=</span> mediaSession
<span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre>
<p>如上面的代码段, 这便是一个最简单的 Media3 播放器实例了。我们这里使用的是实现播放功能最简单的 MediaSessionService, 但通常情况下最好使用 MediaLibraryService。MediaLibraryService 能够为其他的使用暴露播放器的媒体库 (Android Auto, 语音助手)。</p>
<p>让我们逐行分解。</p>
<ol>
<li>MediaSession</li>
</ol>
<pre class="line-numbers language-kotlin" data-language="kotlin"><code class="language-kotlin"><span class="token keyword">private</span> <span class="token keyword">var</span> mediaSession<span class="token operator">:</span> MediaSession<span class="token operator">?</span> <span class="token operator">=</span> <span class="token keyword">null</span>
<span class="token comment">/*
...
*/</span>
<span class="token keyword">override</span> <span class="token keyword">fun</span> <span class="token function">onGetSession</span><span class="token punctuation">(</span>controllerInfo<span class="token operator">:</span> MediaSession<span class="token punctuation">.</span>ControllerInfo<span class="token punctuation">)</span><span class="token operator">:</span> MediaSession<span class="token operator">?</span> <span class="token operator">=</span> mediaSession<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre>
<p>我们在这里定义了一个 mediaSession。这个 session 将作为 Activity 的 UI 层与 Service 层交互的凭证。 Activity 层可以凭借这个 Session 来与播放器通讯, 添加播放清单等。</p>
<ol start="2">
<li>Player<pre class="line-numbers language-kotlin" data-language="kotlin"><code class="language-kotlin"><span class="token keyword">val</span> player <span class="token operator">=</span> ExoPlayer<span class="token punctuation">.</span><span class="token function">Builder</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token comment">// 处理 Audio Focus</span>
<span class="token keyword">val</span> audioAttributes<span class="token operator">:</span> AudioAttributes <span class="token operator">=</span> AudioAttributes<span class="token punctuation">.</span><span class="token function">Builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">setUsage</span><span class="token punctuation">(</span>C<span class="token punctuation">.</span>USAGE_MEDIA<span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">setContentType</span><span class="token punctuation">(</span>C<span class="token punctuation">.</span>AUDIO_CONTENT_TYPE_MUSIC<span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
player<span class="token punctuation">.</span><span class="token function">setAudioAttributes</span><span class="token punctuation">(</span>audioAttributes<span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></li>
</ol>
<p>我们在这里创建了一个 ExoPlayer 的实例。请注意,此处的 ExoPlayer 非彼 ExoPlayer。 ExoPlayer 作为 Media3 的一部分被包含在了 Media3 当中。<br>而 AudioAttributes 是我们加给 ExoPlayer 的媒体信息,这样当其他的应用需要播放媒体的时候, ExoPlayer 便会自动暂停,当别的应用释放媒体焦点后 ExoPlayer 再自动获取。之前需要用到 AudioManager 来手动处理这一点,现在可以直接通过 AudioAttributes 直接解决。</p>
<p><strong>活动端</strong></p>
<p>活动端的实现很简单,我们只需要在 onStart 中添加以下获取 session 的方法即可。</p>
<pre class="line-numbers language-kotlin" data-language="kotlin"><code class="language-kotlin">sessionToken <span class="token operator">=</span> <span class="token function">SessionToken</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> <span class="token function">ComponentName</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span>GramophonePlaybackService<span class="token operator">::</span><span class="token keyword">class</span><span class="token punctuation">.</span>java<span class="token punctuation">)</span><span class="token punctuation">)</span>
controllerFuture <span class="token operator">=</span> MediaController<span class="token punctuation">.</span><span class="token function">Builder</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> sessionToken<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">buildAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre>
<p>在这里我们获取了一个 controllerFuture,而我们可以通过它来获取 Player 本体。</p>
<pre class="line-numbers language-kotlin" data-language="kotlin"><code class="language-kotlin"><span class="token keyword">val</span> player <span class="token operator">=</span> controllerFuture<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre>
<p>举个例子,假如我们想要暂停播放的话,就可以用 <code>player.pause()</code> 来实现。如果以前我们想要更新 UI 内容的话可能会要用到 Handler 或者 Thread。现在我们只需要给 Player 新增一个回调即可:</p>
<pre class="line-numbers language-kotlin" data-language="kotlin"><code class="language-kotlin"><span class="token keyword">val</span> playerListener <span class="token operator">=</span> <span class="token keyword">object</span> <span class="token operator">:</span> Player<span class="token punctuation">.</span><span class="token function">Listener</span> <span class="token punctuation">{</span>
<span class="token keyword">override</span> <span class="token keyword">fun</span> <span class="token function">onIsPlayingChanged</span><span class="token punctuation">(</span>isPlaying<span class="token operator">:</span> Boolean<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">onIsPlayingChanged</span><span class="token punctuation">(</span>isPlaying<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre>
<pre class="line-numbers language-kotlin" data-language="kotlin"><code class="language-kotlin">controllerFuture<span class="token punctuation">.</span><span class="token function">addListener</span><span class="token punctuation">(</span>
<span class="token punctuation">{</span> controllerFuture<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addListener</span><span class="token punctuation">(</span>playerListener<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
MoreExecutors<span class="token punctuation">.</span><span class="token function">directExecutor</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></div></article></div></div><div class="column column-left is-4-tablet is-4-desktop is-4-widescreen order-1"><div class="card widget" data-type="profile"><div class="card-content"><nav class="level"><div class="level-item has-text-centered flex-shrink-1"><div><figure class="image is-128x128 mx-auto mb-2"><img class="avatar is-rounded" src="https://www.gravatar.com/avatar/5f8f47a500e53d5a525bbb203f362068?s=128" alt="AkaneTan"></figure><p class="title is-size-4 is-block" style="line-height:inherit;">AkaneTan</p><p class="is-size-6 is-block">Nekoverse wanderer</p><p class="is-size-6 is-flex justify-content-center"><i class="fas fa-map-marker-alt mr-1"></i><span>Shanghai, China</span></p></div></div></nav><nav class="level is-mobile"><div class="level-item has-text-centered is-marginless"><div><p class="heading">文章</p><a href="/archives"><p class="title">1</p></a></div></div><div class="level-item has-text-centered is-marginless"><div><p class="heading">分类</p><a href="/categories"><p class="title">1</p></a></div></div><div class="level-item has-text-centered is-marginless"><div><p class="heading">标签</p><a href="/tags"><p class="title">3</p></a></div></div></nav><div class="level"><a class="level-item button is-primary is-rounded" href="https://github.com/AkaneTan" target="_blank" rel="noopener">关注我</a></div><div class="level is-mobile is-multiline"><a class="level-item button is-transparent is-marginless" target="_blank" rel="noopener" title="Github" href="https://github.com/AkaneTan"><i class="fab fa-github"></i></a><a class="level-item button is-transparent is-marginless" target="_blank" rel="noopener" title="Twitter" href="https://twitter.com/RealAkaneTan"><i class="fab fa-twitter"></i></a><a class="level-item button is-transparent is-marginless" target="_blank" rel="noopener" title="Mastodon" href="https://mastodon.social/@AkaneTan"><i class="fab fa-mastodon"></i></a><a class="level-item button is-transparent is-marginless" target="_blank" rel="noopener" title="RSS" href="/"><i class="fas fa-rss"></i></a></div></div></div><!--!--><div class="card widget" data-type="links"><div class="card-content"><div class="menu"><h3 class="menu-label">链接</h3><ul class="menu-list"><li><a class="level is-mobile" href="https://github.com/Droid-ng" target="_blank" rel="noopener"><span class="level-left"><span class="level-item">Droid-ng</span></span><span class="level-right"><span class="level-item tag">github.com</span></span></a></li><li><a class="level is-mobile" href="https://libremobileos.com" target="_blank" rel="noopener"><span class="level-left"><span class="level-item">LMODroid</span></span><span class="level-right"><span class="level-item tag">libremobileos.com</span></span></a></li></ul></div></div></div><div class="card widget" data-type="categories"><div class="card-content"><div class="menu"><h3 class="menu-label">分类</h3><ul class="menu-list"><li><a class="level is-mobile" href="/categories/Technical/"><span class="level-start"><span class="level-item">Technical</span></span><span class="level-end"><span class="level-item tag">1</span></span></a></li></ul></div></div></div><div class="card widget" data-type="recent-posts"><div class="card-content"><h3 class="menu-label">最新文章</h3><article class="media"><div class="media-content"><p class="date"><time dateTime="2023-09-24T16:15:50.000Z">2023-09-25</time></p><p class="title"><a href="/2023/09/25/%E6%B5%85%E8%B0%88media3%E7%9A%84%E5%AE%9E%E7%8E%B0/">浅谈 Media3 (一)</a></p><p class="categories"><a href="/categories/Technical/">Technical</a></p></div></article></div></div><div class="card widget" data-type="archives"><div class="card-content"><div class="menu"><h3 class="menu-label">归档</h3><ul class="menu-list"><li><a class="level is-mobile" href="/archives/2023/"><span class="level-start"><span class="level-item">2023</span></span><span class="level-end"><span class="level-item tag">1</span></span></a></li></ul></div></div></div><div class="card widget" data-type="tags"><div class="card-content"><div class="menu"><h3 class="menu-label">标签</h3><div class="field is-grouped is-grouped-multiline"><div class="control"><a class="tags has-addons" href="/tags/Android/"><span class="tag">Android</span><span class="tag">1</span></a></div><div class="control"><a class="tags has-addons" href="/tags/Kotlin/"><span class="tag">Kotlin</span><span class="tag">1</span></a></div><div class="control"><a class="tags has-addons" href="/tags/Media3/"><span class="tag">Media3</span><span class="tag">1</span></a></div></div></div></div></div></div><!--!--></div></div></section><footer class="footer"><div class="container"><div class="level"><div class="level-start"><a class="footer-logo is-block mb-2" href="/">Foundation Chronicle</a><p class="is-size-7"><span>© 2023 Shinjo Akane</span> Powered by <a href="https://hexo.io/" target="_blank" rel="noopener">Hexo</a> & <a href="https://github.com/ppoffice/hexo-theme-icarus" target="_blank" rel="noopener">Icarus</a></p><p class="is-size-7">© 2022-2023 Akane Foundation</p></div><div class="level-end"><div class="field has-addons"><p class="control"><a class="button is-transparent is-large" target="_blank" rel="noopener" title="Creative Commons" href="https://creativecommons.org/"><i class="fab fa-creative-commons"></i></a></p><p class="control"><a class="button is-transparent is-large" target="_blank" rel="noopener" title="Attribution 4.0 International" href="https://creativecommons.org/licenses/by/4.0/"><i class="fab fa-creative-commons-by"></i></a></p><p class="control"><a class="button is-transparent is-large" target="_blank" rel="noopener" title="Github" href="https://github.com/AkaneTan"><i class="fab fa-github"></i></a></p></div></div></div></div></footer><script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script><script src="https://cdn.jsdelivr.net/npm/[email protected]/min/moment-with-locales.min.js"></script><script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/clipboard.min.js" defer></script><script>moment.locale("zh-cn");</script><script>var IcarusThemeSettings = {
article: {
highlight: {
clipboard: true,
fold: 'unfolded'
}
}
};</script><script src="/js/column.js"></script><script src="/js/animation.js"></script><a id="back-to-top" title="回到顶端" href="javascript:;"><i class="fas fa-chevron-up"></i></a><script src="/js/back_to_top.js" defer></script><!--!--><!--!--><!--!--><script src="https://cdn.jsdelivr.net/npm/[email protected]/build/cookieconsent.min.js" defer></script><script>window.addEventListener("load", () => {
window.cookieconsent.initialise({
type: "info",
theme: "edgeless",
static: false,
position: "bottom-left",
content: {
message: "此网站使用Cookie来改善您的体验。",
dismiss: "知道了!",
allow: "允许使用Cookie",
deny: "拒绝",
link: "了解更多",
policy: "Cookie政策",
href: "https://www.cookiesandyou.com/",
},
palette: {
popup: {
background: "#edeff5",
text: "#838391"
},
button: {
background: "#4b81e8"
},
},
});
});</script><script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/lightgallery.min.js" defer></script><script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/jquery.justifiedGallery.min.js" defer></script><script>window.addEventListener("load", () => {
if (typeof $.fn.lightGallery === 'function') {
$('.article').lightGallery({ selector: '.gallery-item' });
}
if (typeof $.fn.justifiedGallery === 'function') {
if ($('.justified-gallery > p > .gallery-item').length) {
$('.justified-gallery > p > .gallery-item').unwrap();
}
$('.justified-gallery').justifiedGallery();
}
});</script><!--!--><!--!--><!--!--><!--!--><!--!--><script src="/js/main.js" defer></script><div class="searchbox"><div class="searchbox-container"><div class="searchbox-header"><div class="searchbox-input-container"><input class="searchbox-input" type="text" placeholder="想要查找什么..."></div><a class="searchbox-close" href="javascript:;">×</a></div><div class="searchbox-body"></div></div></div><script src="/js/insight.js" defer></script><script>document.addEventListener('DOMContentLoaded', function () {
loadInsight({"contentUrl":"/content.json"}, {"hint":"想要查找什么...","untitled":"(无标题)","posts":"文章","pages":"页面","categories":"分类","tags":"标签"});
});</script></body></html>