feat: add Android Report
7
AndroidReport/.latexmkrc
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
$pdf_mode = 1;
|
||||||
|
$pdflatex = "xelatex -file-line-error --shell-escape -src-specials -synctex=1 -interaction=nonstopmode %O %S;cp %D %R.pdf";
|
||||||
|
$recorder = 1;
|
||||||
|
$clean_ext = "synctex.gz acn acr alg aux bbl bcf blg brf fdb_latexmk glg glo gls idx ilg ind ist lof log lot out run.xml toc dvi";
|
||||||
|
$bibtex_use = 2;
|
||||||
|
$out_dir = "temp";
|
||||||
|
$jobname = "AndroidReport";
|
BIN
AndroidReport/assets/album-page.png
Normal file
After Width: | Height: | Size: 816 KiB |
BIN
AndroidReport/assets/components.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
AndroidReport/assets/compose-example.png
Normal file
After Width: | Height: | Size: 128 KiB |
BIN
AndroidReport/assets/composition.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
AndroidReport/assets/declarative.png
Normal file
After Width: | Height: | Size: 130 KiB |
BIN
AndroidReport/assets/main-music-page.png
Normal file
After Width: | Height: | Size: 893 KiB |
BIN
AndroidReport/assets/main-video-page.png
Normal file
After Width: | Height: | Size: 122 KiB |
BIN
AndroidReport/assets/media-components.png
Normal file
After Width: | Height: | Size: 458 KiB |
BIN
AndroidReport/assets/media3-architecture.png
Normal file
After Width: | Height: | Size: 232 KiB |
BIN
AndroidReport/assets/playlist-page.png
Normal file
After Width: | Height: | Size: 871 KiB |
BIN
AndroidReport/assets/server-architecture.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
AndroidReport/assets/show-page.png
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
AndroidReport/assets/song-page.png
Normal file
After Width: | Height: | Size: 882 KiB |
BIN
AndroidReport/assets/song-playing.png
Normal file
After Width: | Height: | Size: 905 KiB |
BIN
AndroidReport/assets/system-architecture.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
AndroidReport/assets/video-page.png
Normal file
After Width: | Height: | Size: 670 KiB |
371
AndroidReport/ch01-technology.tex
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
\documentclass[main.tex]{subfiles}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
\section{相关技术}
|
||||||
|
|
||||||
|
在本节中,我们将介绍本系统开发过程中涉及的相关技术进行总体概述。其中主要包括系统开发的底层框架Jetpack Compose和多媒体播放中使用的核心协议HLS协议,还有一些辅助开发的工具技术,例如依赖注入和ExoPlayer播放器。
|
||||||
|
|
||||||
|
% 对本系统设计开发涉及的相关技术进行重点描述
|
||||||
|
|
||||||
|
\subsection{Jetpack Compose}
|
||||||
|
|
||||||
|
不同与以往Android开发中的使用Java语言进行编程和基于XML文件编写界面的传统,现代Android开发更加推崇的是使用Kotlin语言进行编程和使用Compose库进行界面开发。在新式语言的加持下,Jetpack Compose库提供了和以往完全不同的开发体验。鉴于新式的Jetpack库均使用Kotlin进行编写,并且在开发过程中大量利用了kotlin的语言特性辅助开发,我们将首先介绍一下Kotlin语言。
|
||||||
|
|
||||||
|
\subsubsection{Kotlin编程语言}
|
||||||
|
|
||||||
|
Kotlin是一种现代的、静态类型的编程语言,由JetBrains团队开发并开源。它最初于2011年发布,旨在解决Java语言中的一些痛点,同时保持与Java的兼容性。Kotlin的设计目标是提高开发效率、代码可读性和安全性,同时减少冗余和样板代码。\cite{kotlin_docs}
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item 语法简洁。Kotlin的语法设计简洁明了,很多常见的操作都可以用更少的代码实现。例如,函数可以被定义为表达式,省去了大括号和return语句。此外,Kotlin引入了数据类、解构声明、范围表达式等特性,使得处理数据结构和控制流变得更加直观。
|
||||||
|
\item 静态类型与类型推断。尽管Kotlin是一种静态类型的语言,但它通过强大的类型推断机制大大减少了类型声明的需要,这使得代码更加简洁。同时,Kotlin支持类型安全的空值检查和非空断言,有效避免了运行时的空指针异常。
|
||||||
|
\item 安全性与可空性。Kotlin在设计上非常注重安全性,尤其是针对空指针异常这一常见问题。在Kotlin中,所有变量默认都是非空的,如果一个变量可能为空,必须显式声明为可空类型。这种设计强迫开发者在编码时就考虑到空值的情况,从而在编译阶段就能捕获潜在的错误。
|
||||||
|
\item 函数式编程支持。Kotlin支持函数式编程风格,包括高阶函数、lambda表达式、集合操作等。这使得编写简洁、高效的代码成为可能,同时也便于利用现代多核处理器进行并行计算。
|
||||||
|
\item 并发与协程。Kotlin提供了协程(Coroutine)来处理并发编程,这是一种轻量级的线程管理方式。协程允许你以同步的方式编写异步代码,极大地简化了复杂的并发逻辑,同时避免了回调地狱。
|
||||||
|
\item 生态系统与工具链。 Kotlin与Java生态系统高度兼容,这意味着现有的Java库可以直接在Kotlin项目中使用。同时,Kotlin也拥有自己丰富的库和框架,如Ktor(用于Web开发)、KMM(跨平台移动开发)等。JetBrains的IDE,如IntelliJ IDEA,提供了优秀的Kotlin支持,包括代码编辑、调试和重构工具。
|
||||||
|
\item 跨平台能力。Kotlin不仅仅局限于JVM平台,它还支持原生编译到其他平台,如iOS、Android和WebAssembly。这意味着你可以用Kotlin编写一次代码,然后部署到多个平台上,极大地提高了开发效率。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
在上述提到的若干Kotlin语言特点中,对于函数式编程的支持和提供了协程的概念是在现代Android开发中非常常用的两点,尤其是对于函数式编程的支持,几乎是Jetpack Compose库的基石,区别与完全使用面向对象范式进行设计的Java语言,Kotlin在设计之初就融合了面向对象和函数式的编程范式,提供了一系列强大的功能,下面简述一些在开发过程中常用的函数式编程特性。
|
||||||
|
|
||||||
|
\begin{enumerate}
|
||||||
|
\item \textbf{Lambda表达式}
|
||||||
|
|
||||||
|
Kotlin 支持 lambda 表达式,这是函数式编程中的基本概念。Lambda 允许你将函数作为参数传递给其他函数,或者作为返回值从函数中返回。Kotlin 的 lambda 表达式语法简洁,可以自动推断类型。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
val numbers = listOf(1, 2, 3, 4, 5)
|
||||||
|
val evenNumbers = numbers.filter { it % 2 == 0 } // 使用 lambda 过滤偶数
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\item \textbf{高阶函数}
|
||||||
|
|
||||||
|
高阶函数是指可以接受函数作为参数或返回函数作为结果的函数。Kotlin 提供了许多内置的高阶函数,如 map、filter、reduce 等,这些函数使得集合操作变得简单而高效。
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
val numbers = listOf(1, 2, 3, 4, 5)
|
||||||
|
val sumOfSquares = numbers.map { it * it }.reduce { acc, n -> acc + n }
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\item \textbf{内联函数和扩展函数}
|
||||||
|
|
||||||
|
Kotlin 的内联函数可以避免 lambda 表达式带来的额外开销,提高性能。内联函数通过在编译时将函数体嵌入调用点来实现这一点。扩展函数则允许你在不修改原始类的情况下,为其添加新方法,这对于函数式编程中的组合和重用特别有用。
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
inline fun logAction(action: () -> Unit) {
|
||||||
|
println("Action started.")
|
||||||
|
action()
|
||||||
|
println("Action completed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
logAction {
|
||||||
|
// 执行一些操作
|
||||||
|
println("Doing something important...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
data class Person(val name: String, val age: Int)
|
||||||
|
// 扩展函数,为 Person 类添加一个 printInfo 方法
|
||||||
|
fun Person.printInfo() {
|
||||||
|
println("Name: $name, Age: $age")
|
||||||
|
}
|
||||||
|
fun main() {
|
||||||
|
val person = Person("Alice", 30)
|
||||||
|
person.printInfo() // 输出:Name: Alice, Age: 30
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
\subsubsection{Compose UI框架}
|
||||||
|
|
||||||
|
Jetpack Compose是Google推出的一款用于构建Android UI的现代声明式UI框架。它旨在简化用户界面的开发流程,提高开发效率,并且提供了更丰富的交互和动画能力。\cite{compose_docs}
|
||||||
|
|
||||||
|
\begin{figure}
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.9\linewidth]{assets/declarative.png}
|
||||||
|
\caption{声明式的编程模型}
|
||||||
|
\label{fig:declarative}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
Jetpack Compose采用了声明式的编程模型,这意味着开发者只需描述UI在特定状态下的样子,而不需要明确地指定如何绘制或更新UI。当应用程序的状态发生变化时,Jetpack Compose会自动计算出UI的更新部分,并有效地重新绘制屏幕。这种模型与传统的命令式UI框架(如基于View的Android UI)形成了鲜明对比。
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.9\linewidth]{assets/composition.png}
|
||||||
|
\caption{可组合式函数示意}
|
||||||
|
\label{fig:composition}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
Jetpack Compose的核心概念是可组合函数(@Composable),这些函数被注解修饰,可以创建和组织UI元素。可组合函数可以嵌套调用,形成复杂的UI布局。它们类似于React中的组件,但更专注于描述UI的局部片段,而非整个组件树。例如对于下面这段代码,其就直接描述了图\ref{fig:compose-example}中的UI树。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
@Composable
|
||||||
|
fun MyComposable() {
|
||||||
|
Column {
|
||||||
|
Text("Hello")
|
||||||
|
Text("World")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\begin{figure}
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.9\linewidth]{assets/compose-example.png}
|
||||||
|
\caption{MyComposable函数声明的UI树}
|
||||||
|
\label{fig:compose-example}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
Jetpack Compose提供了管理状态的机制,包括使用remember和mutableStateOf等函数来存储和更新UI相关的状态。同时,它还提供了处理副作用(如启动动画、发送网络请求)的机制,如LaunchedEffect和DisposableEffect。
|
||||||
|
|
||||||
|
Jetpack Compose的很多核心能力,比如将可组合函数转换成有效的UI渲染指令,都是通过Kotlin编译器插件实现的。这些插件在编译阶段处理Composable函数,生成优化过的代码,以提高运行时性能。
|
||||||
|
|
||||||
|
Jetpack Compose提供了一套完整的动画API,可以轻松创建复杂的动画效果,包括过渡动画、属性动画和自定义动画。这些动画可以无缝集成到UI的构建过程中,无需额外的动画框架或库。
|
||||||
|
|
||||||
|
Jetpack Compose内置了Material Design组件,使得遵循Material Design规范的UI设计变得容易。这些组件包括按钮、文本字段、滑块等,它们可以被直接在可组合函数中使用。
|
||||||
|
|
||||||
|
Jetpack Compose使用了被称为\textbf{单向数据流}的软件工程设计模式,特别是前端框架中用于管理组件间数据流动的设计模式。这种模式强调数据的流动是单向的,即数据只沿着一个固定的方向传递,而不是在组件之间来回双向传递。这种模式有助于保持数据流的清晰和可预测性,使得应用的状态更容易理解和维护。
|
||||||
|
|
||||||
|
单向数据流模式具有如下的特点。一是数据从父组件传递到子组件,通常通过属性(props)实现。子组件通过这些属性接收数据,并将其用于渲染。二是子组件不能直接修改从父组件接收的数据。如果需要与父组件交互,子组件会触发事件(通常通过回调函数),这些事件携带的信息会被父组件捕获并作出响应。三是对于更复杂的应用,可能需要在更高层次上集中管理状态。这样,组件可以请求更新状态,但状态的变更由中心化的控制器控制,确保数据流的单向性。
|
||||||
|
|
||||||
|
使用单向数据流模式对于相对于之前的Android界面设计模型(即允许数据的双向绑定),带来了这些好处:
|
||||||
|
\begin{itemize}
|
||||||
|
\item \textbf{可预测性}:由于数据流的路径是固定的,因此更容易追踪数据变化,预测应用的行为。
|
||||||
|
\item \textbf{易于调试}:当数据只沿一个方向流动时,调试问题会更加直接,因为不需要考虑数据的多源头或多目标。
|
||||||
|
\item \textbf{模块化}:组件可以独立于其他组件编写和测试,因为它们的输入和输出是明确定义的。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\subsection{依赖注入}
|
||||||
|
|
||||||
|
依赖注入(Dependency Injection,DI)和控制反转(Inversion of Control,IoC)是软件工程中,特别是在面向对象编程中,用于降低代码耦合度和提高模块间解耦的重要设计模式和原则。
|
||||||
|
|
||||||
|
控制反转是一种设计原则,它描述了软件组件控制流的反转。在传统的编程模式下,组件控制着它们的依赖项的创建和生命周期;而在IoC中,这种控制权被“反转”给了外部容器或框架。这意味着组件不再负责初始化和管理其依赖,而是由外部实体(通常是一个框架或容器)来承担这一职责。IoC的目标是降低组件间的耦合,使得各个组件更加独立,易于测试和维护。
|
||||||
|
|
||||||
|
依赖注入是实现控制反转的一种常用手段。它是一种设计模式,其中对象的依赖关系不在对象内部创建和管理,而是在对象被创建时由外部实体(如IoC容器)注入。依赖注入有几种不同的形式:
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item 构造器注入(Constructor Injection):依赖项通过构造函数的参数传递给对象。这是最推荐的方式,因为它可以保证依赖项的不可变性,且易于单元测试。
|
||||||
|
\item 属性注入(Property Injection):依赖项通过setter方法或公共属性在对象创建后注入。这在某些情况下可能不太安全,因为依赖项可能在对象使用前没有正确设置。
|
||||||
|
\item 方法注入(Method Injection):依赖项通过方法调用在需要的时候注入。这种方法较少见,通常不推荐,因为它可能导致代码的可读性和可维护性下降。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
依赖项注入为应用的开发带来了如下的优势:
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item 重用类以及分离依赖项:更容易换掉依赖项的实现。由于控制反转,代码重用得以改进,并且类不再控制其依赖项的创建方式,而是支持任何配置。
|
||||||
|
\item 易于重构:依赖项成为 API Surface 的可验证部分,因此可以在创建对象时或编译时进行检查,而不是作为实现详情隐藏。
|
||||||
|
\item 易于测试:类不管理其依赖项,因此在测试时,您可以传入不同的实现以测试所有不同用例。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
在Android开发中,Hilt是一个非常流行的依赖注入框架,它由Google维护,基于Dagger 2构建,但提供了更简化和更易于使用的API,尤其针对Android的开发环境进行了优化。Hilt的主要目标是减少依赖注入所需的样板代码,使得依赖注入更加直观和易于理解。
|
||||||
|
|
||||||
|
Hilt主要涉及以下几个概念:
|
||||||
|
\begin{itemize}
|
||||||
|
\item 依赖注入:在软件工程中,依赖注入(Dependency Injection, DI)是一种设计模式,它允许你将依赖项的创建和管理委托给外部实体,而不是在类内部创建。这样可以降低类之间的耦合度,使得代码更易于测试和维护。
|
||||||
|
\item 作用域:Hilt支持作用域,这意味着你可以定义依赖项的生命周期。例如,你可能希望在Activity的生命周期内仅创建一次依赖项,或者在整个应用程序的生命周期中共享一个依赖项。
|
||||||
|
\item 组件和模块:Hilt使用组件和模块来组织和配置依赖项。组件负责依赖项的创建和管理,而模块则定义了依赖项的绑定规则。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\subsection{Jetpack Media3}
|
||||||
|
|
||||||
|
Jetpack Media3是Google为Android平台提供的一套多媒体框架,它是Media2的继任者,旨在提供更现代化、更灵活、更强大的媒体播放能力。Media3主要聚焦于音频和视频播放,它包含了一系列的库,旨在帮助开发者更轻松地集成高质量的媒体播放体验到他们的应用中。
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.9\linewidth]{assets/media-components.png}
|
||||||
|
\caption{媒体组件组合示例}
|
||||||
|
\label{fig:media-components}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
Media3多媒体框架提供了如下一些特点:
|
||||||
|
\begin{itemize}
|
||||||
|
\item 模块化设计:Media3被设计成一系列模块,这允许开发者根据自己的需求选择和集成特定的组件,比如播放器、数据源、解码器等,而不是被迫使用整个框架。
|
||||||
|
\item ExoPlayer集成:Media3的核心是ExoPlayer,这是一个高性能的开源媒体播放器库,能够支持广泛的媒体格式和流媒体协议,如MP4、WebM、FLAC、MP3、AAC、Ogg、TS、WAV等。
|
||||||
|
\item API一致性:Media3提供了一致的API,使得开发者可以轻松地在不同的媒体播放场景中切换播放器的实现,而无需大量修改代码。
|
||||||
|
\item 向后兼容性:Media3致力于提供向后兼容的API,确保它可以运行在广泛的Android设备上,包括较旧的设备。
|
||||||
|
\item 高级功能:Media3支持多种高级功能,如DRM(数字版权管理)、字幕、多音轨、画中画模式、HDR视频等。
|
||||||
|
\item 易用性和可定制性:Media3提供了一个易于使用的API,同时也允许开发者深度定制播放器的行为和外观,以满足特定的应用需求。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
Media3为播放提供了一些关键组件,这些组件大大降低的编写多媒体播放器的复杂度。图\ref{fig:media-components}说明了这些组件在典型应用如何组合在一起。
|
||||||
|
|
||||||
|
\paragraph{媒体播放器} 媒体播放器是应用中允许播放媒体文件的组件。
|
||||||
|
|
||||||
|
\begin{table*}[htbp]
|
||||||
|
\centering
|
||||||
|
\begin{tabularx}{\textwidth}{|l|X|X|}
|
||||||
|
\hline
|
||||||
|
类 & 说明 & 实现注意事项 \\
|
||||||
|
\hline
|
||||||
|
Player & Player 是一个接口,定义了媒体播放器的传统高级功能,如播放、暂停和跳转功能。& 在 Media3 中,Player 接口是多个组件(例如 MediaSession 和 MediaController)实现或使用的常见 API。\\
|
||||||
|
\hline
|
||||||
|
ExoPlayer & ExoPlayer 是 Media3 中 Player 接口的默认实现。 & \\
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
\caption{媒体播放器组件}
|
||||||
|
\label{tab:my_label}
|
||||||
|
\end{table*}
|
||||||
|
|
||||||
|
\paragraph{媒体会话} 媒体会话提供了一种与媒体播放器互动的通用方式。这样,应用就可以向外部来源通告媒体播放,并接收来自外部来源的播放控制请求。
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.9\linewidth]{assets/media3-architecture.png}
|
||||||
|
\caption{Media3架构图}
|
||||||
|
\label{fig:media-architecture}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
\begin{table*}[htbp]
|
||||||
|
\centering
|
||||||
|
\begin{tabularx}{\textwidth}{|l|X|X|}
|
||||||
|
\hline
|
||||||
|
类 & 说明 & 实现注意事项 \\
|
||||||
|
\hline
|
||||||
|
MediaSession & 媒体会话可让您的应用与音频或视频播放器互动。它们在外部通告媒体播放,并从外部来源接收播放命令。 & 在 Media3 中,MediaSession 需要 Player 才能执行命令并获取当前状态。 \\
|
||||||
|
\hline
|
||||||
|
MediaSessionService & MediaSessionService 将媒体会话及其关联的播放器保存在与应用的主 Activity不同的服务中,以便于后台播放。 & \\
|
||||||
|
\hline
|
||||||
|
MediaController & MediaController 类通常用于从应用外部发送命令,例如从其他应用或系统本身发送命令。这些命令会被发送到关联 MediaSession 的底层 Player 。 & MediaController 类实现了 Player 接口,但在调用方法时,该命令会被发送到已连接的 MediaSession。诸如 Google 助理等客户端应用可以使用 MediaController 在已连接的会话中控制播放。 \\
|
||||||
|
\hline
|
||||||
|
MediaLibraryService & MediaLibraryService 与 MediaSessionService 类似,只不过它包含额外的 API,以便您将内容库提供给客户端应用。& \\
|
||||||
|
\hline
|
||||||
|
MediaBrowser & 通过 MediaBrowser 类,用户可以浏览媒体应用的内容库,并选择要播放的内容。 & MediaBrowser 类同时实现了 MediaController 和 Player 接口。与 MediaController 类似,Android Auto 等客户端应用通常会实现 MediaBrowser。\\
|
||||||
|
\hline
|
||||||
|
\end{tabularx}
|
||||||
|
\caption{媒体会话组件}
|
||||||
|
\end{table*}
|
||||||
|
|
||||||
|
在实际的开发过程中,媒体会话是完成Activity和Service之间通信的重要组件。首先由于性能的原因,为了保证UI界面的响应性,我们往往会将媒体播放的逻辑放置到一个单独的Service上去运行,这就引入了Activity和Service之间通信的问题,在传统的开发过程中,我们可能需要IBinder或者是Messager来完成进程间的通信,亦或使用广播的方式传递消息。但是在Media3框架中,MediaSession的引入为我们完成了这一系列的任务,从图\ref{fig:media-architecture}中可以很清楚的看出这一关系。
|
||||||
|
|
||||||
|
\subsection{HLS}
|
||||||
|
|
||||||
|
HLS,全称为HTTP Live Streaming,是由苹果公司提出并开发的一种基于HTTP的流媒体网络传输协议。HLS的主要目的是实现实时音视频流的传输,尤其是在网络条件变化较大的环境中,它能提供流畅且自适应的流媒体播放体验。
|
||||||
|
|
||||||
|
HLS具有一下关键特点:
|
||||||
|
\begin{itemize}
|
||||||
|
\item 自适应码率:HLS能够根据网络状况自动调整视频的比特率,这意味着在网络条件不佳时,视频质量会自动降低以保持流畅播放,而在网络良好时,视频质量会提升。
|
||||||
|
\item 基于HTTP的传输:使用HTTP作为传输协议,HLS可以利用现有的HTTP基础设施,这使得它易于部署,不需要专门的流媒体服务器硬件。
|
||||||
|
\item 媒体文件切片:HLS将媒体内容分割成一系列短小的基于HTTP的媒体片段,每个片段都可以独立下载。这不仅提高了容错能力,还允许快速频道切换。
|
||||||
|
\item 清单文件(Playlist):HLS使用一个M3U8格式的文本文件作为清单,其中包含了指向媒体片段的URL链接以及元数据。客户端通过解析这个清单文件来获取并播放媒体片段。
|
||||||
|
\item 广泛的支持:HLS不仅被苹果的设备和平台所支持,也被许多其他厂商和流媒体播放器所采纳,包括Android设备、智能电视、游戏机和网页浏览器等。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
HLS的工作流程通常如下:服务器将视频流切分为多个小的TS(Transport Stream)文件,并生成一个M3U8格式的清单文件。客户端(如iOS设备或Safari浏览器)首先请求M3U8清单文件,然后根据当前网络状况选择合适的比特率流开始播放。当客户端检测到网络状况变化时,它会自动选择不同比特率的流,以维持播放的连续性和流畅性。
|
||||||
|
|
||||||
|
\subsubsection{FFmpeg}
|
||||||
|
|
||||||
|
FFmpeg是一个极其强大且功能全面的开源软件项目,主要用于处理多媒体数据,包括音频和视频。它被广泛地用于多种场景,如流媒体服务、转码服务、开发多媒体应用以及个人和专业级别的音视频处理任务。
|
||||||
|
|
||||||
|
以下是FFmpeg的一些关键特性:
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item 编码和解码:FFmpeg支持大量的音频和视频编解码器,这使得它可以处理几乎所有的主流格式,从古老的编码方式到最新的编码标准,如H.264、H.265(HEVC)、VP9、AAC、MP3等。
|
||||||
|
\item 容器格式:它能够读取和写入多种容器格式,如MP4、AVI、MKV、FLV、TS、MPEG等,这意味着你可以将不同格式的音视频文件转换为其他格式。
|
||||||
|
\item 流媒体:FFmpeg可以将音视频流化,支持RTSP、HTTP、RTMP等多种网络协议,使得实现实时流媒体传输成为可能。
|
||||||
|
\item 过滤器系统:FFmpeg提供了一套强大的音频和视频过滤器,允许对音视频进行复杂处理,如裁剪、缩放、旋转、颜色调整、去噪、混音等。
|
||||||
|
\item GPU加速:它利用硬件加速,如CUDA、OpenCL和VAAPI,来提升视频处理性能。
|
||||||
|
\item 跨平台:FFmpeg在各种操作系统上都可以运行,包括Linux、Windows、macOS、BSD和Solaris等。
|
||||||
|
\item 库支持:FFmpeg的核心组件包括libavcodec(编解码器库)、libavformat(封装器库)、libavutil(通用工具库)、libavfilter(过滤器库)和libavdevice(设备输入输出库),这些库可以被其他软件集成以实现多媒体处理功能。
|
||||||
|
\item 命令行工具:FFmpeg提供了一系列命令行工具,如ffmpeg(用于转换和流化)、ffplay(一个简单的媒体播放器)和ffprobe(用于检查媒体文件的元数据)。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
在系统中主要涉及到使用FFmpeg生成HLS(HTTP Live Streaming)流涉及到将视频内容分割成一系列较小的HTTP可寻址的文件片段,同时创建一个M3U8索引文件来列出这些片段。下面是系统中使用的命令:
|
||||||
|
|
||||||
|
\begin{lstlisting}
|
||||||
|
ffmpeg -re -i video.mp4 -c:v h264 -f hls -profile:v high10 -hls_list_size 10 -hls_time 10 -hls_base_url /api/hls -hls_flags delete_segments -o output.m3u8
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
命令中参数的含义为:
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item -re: 这个选项让FFmpeg以实时方式读取输入文件,就像从实时源读取数据一样。这在处理实时或流式传输的输入时非常有用,但对于普通的文件输入来说,这个选项可能不是必要的。
|
||||||
|
\item -i video.mp4: 指定要转换的输入视频文件为video.mp4。
|
||||||
|
\item -c:v h264: 指定视频编码器为H.264。这告诉FFmpeg使用H.264编码器对视频进行编码。
|
||||||
|
\item -profile:v high10: 指定H.264编码的配置文件为high10。high10是一个支持10位颜色深度的高级配置文件,通常用于高质量视频,可以提供更细腻的色彩过渡和减少色带效应。
|
||||||
|
\item -f hls: 输出格式为HLS (HTTP Live Streaming),这是一种适应性比特率流媒体协议,适用于通过HTTP服务器进行视频流传输。
|
||||||
|
\item -hls\_list\_size 10: 指定M3U8播放列表中保留的最新片段数量为10。当新的片段被添加时,最早的那个片段会被删除,保持列表中的片段数量为10。
|
||||||
|
\item -hls\_time 10: 设置每个HLS片段的持续时间为10秒。这是HLS片段的默认时长。
|
||||||
|
\item -hls\_base\_url /api/hls: 指定HLS片段的基URL为/api/hls。这意味着生成的M3U8播放列表中的每个片段引用都将以前缀/api/hls开始。
|
||||||
|
\item -hls\_flags delete\_segments: 添加了一个HLS标志,告诉FFmpeg在片段过期后删除它们。这有助于节省磁盘空间,因为不再需要的旧片段会被自动清理。
|
||||||
|
\item -o output.m3u8: 指定输出的M3U8播放列表文件名为output.m3u8。这是HLS流的主要索引文件,客户端会请求这个文件来获取可用的视频片段列表。
|
||||||
|
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
综上所述,这个命令将video.mp4文件转换为HLS格式,使用H.264编码器和high10配置文件进行编码,生成的流将有10个最新的10秒片段,片段的URL将以/api/hls开头,并且过期的片段将会被自动删除。最终的M3U8播放列表文件名为output.m3u8。上述配置是为了实时流媒体服务和需要高质量视频流的应用场景而优化的。
|
||||||
|
|
||||||
|
\subsection{OpenAPI}
|
||||||
|
|
||||||
|
如何保证前端和后端之间的接口一致性的前后端分离的开发模式中非常重要的一点,在本系统中引入了OpenAPI这一框架来解决这一问题。
|
||||||
|
|
||||||
|
OpenAPI(原名Swagger)是一种规范和框架,用于描述API,特别是RESTful API的结构。它提供了一种标准的方式来定义API的行为,包括其路径、参数、模型、响应等,使得API的设计者能够清晰地表达API的功能而无需考虑具体的实现细节。OpenAPI规范使用JSON或YAML格式来描述API,允许开发者、测试人员和API消费者之间进行更有效的沟通和协作。
|
||||||
|
|
||||||
|
以下是OpenAPI的一些关键特点和用途:
|
||||||
|
\begin{itemize}
|
||||||
|
\item 标准化:OpenAPI提供了一套标准化的模式,用于描述API接口,使得API文档可读性强,易于理解,同时也有利于自动化工具的生成和消费。
|
||||||
|
\item 自动生成文档:基于OpenAPI规范,可以自动生成API的交互式文档,如Swagger UI,这极大地提高了开发效率和用户体验,使开发者能够快速理解和测试API。
|
||||||
|
\item 代码生成:OpenAPI还支持从API描述中自动生成服务器端和客户端的代码,支持多种编程语言,如Java、Python、C\#等,从而减少了手动编写代码的工作量,提高了开发效率。
|
||||||
|
\item API测试与验证:OpenAPI规范可以用于自动化API测试,因为它提供了API的完整描述,包括所有路径、参数、响应等,这使得编写测试用例变得简单和直接。
|
||||||
|
\item API发现与注册:OpenAPI规范还可以用于API的发现和注册,使得API能够在一个组织内或跨组织间被轻松查找和复用。
|
||||||
|
\item 生态系统:围绕OpenAPI已经形成了一个丰富的工具和社区生态系统,包括编辑器、IDE插件、代码生成器、测试工具等,这些工具和服务进一步增强了OpenAPI的价值。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
openapi-generator是一个强大的工具,可以基于OpenAPI (Swagger) 规范自动生成服务器存根和客户端库的源代码。openapi-generator的Gradle插件允许你在构建过程中集成代码生成,这样可以自动化地保持你的代码与API规范同步。在系统中使用openapi-generator和Gradle相配合的情况下从后端生成的接口文档中生成前后端通信的胶水代码。在Android项目中使用openapi-generator的过程简述如下:
|
||||||
|
|
||||||
|
首先在build.gradle.kts文件中添加openapi-generator插件依赖。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
plugins {
|
||||||
|
// other plugins
|
||||||
|
alias(libs.plugins.openapi.generator)
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
接下来,在build.gradle.kts中配置openapi-generator插件。这通常涉及到指定API规范的位置以及想要生成的目标语言和输出目录。在系统中的配置代码为:
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
openApiGenerate {
|
||||||
|
generatorName.set("kotlin")
|
||||||
|
inputSpec.set("$rootDir/app/src/main/openapi/chiara.json")
|
||||||
|
outputDir.set(openapiOutputDir)
|
||||||
|
apiPackage.set("top.rrricardo.chiara.openapi.api")
|
||||||
|
modelPackage.set("top.rrricardo.chiara.openapi.model")
|
||||||
|
packageName.set("top.rrricardo.chiara.openapi.client")
|
||||||
|
generateApiTests.set(false)
|
||||||
|
generateModelTests.set(false)
|
||||||
|
configOptions.set(
|
||||||
|
mapOf(
|
||||||
|
"dataLibrary" to "java8"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
additionalProperties.set(
|
||||||
|
mapOf(
|
||||||
|
"library" to "jvm-retrofit2",
|
||||||
|
"serializationLibrary" to "kotlinx_serialization",
|
||||||
|
"useCoroutines" to "true",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
保存build.gradle更改后,在命令行中运行以下命令来执行代码生成:
|
||||||
|
|
||||||
|
\begin{lstlisting}
|
||||||
|
./gradlew openApiGenerate
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
这将会根据输入的OpenAPI规范文件生成代码,并将其放置在指定的输出目录中。一旦生成了代码,将它整合到你的项目中,这需要修改build.gradle.kts文件中对于\texttt{android}进行配置:
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
sourceSets["main"].kotlin {
|
||||||
|
srcDir("$openapiOutputDir/src/main/kotlin")
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
在使用代码生成的过程中,需要注意:
|
||||||
|
|
||||||
|
\begin{enumerate}
|
||||||
|
\item 确保你的OpenAPI规范是最新的,以便生成的代码能够正确反映API的状态。
|
||||||
|
\item 定期运行代码生成任务,特别是在API规范发生改变时,以保持生成的代码与规范同步。
|
||||||
|
\item 如果你需要生成不同语言的代码或者有多个规范文件,你可以在build.gradle.kts中定义多个openApiGenerate任务,每个任务对应一个不同的规范和生成配置。
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
\end{document}
|
82
AndroidReport/ch02-requirements.tex
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
\documentclass[main.tex]{subfiles}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
\section{系统功能需求}
|
||||||
|
|
||||||
|
%系统的功能描述,包括需求分析
|
||||||
|
|
||||||
|
在本节中我们将按照软件工程的方法进行本系统的需求分析,在需求分析中我们介绍系统面向的用户群体,系统应当遵守的标准和规范,并逐条列举系统的功能需求和系统的非功能需求,例如性能需求、环境需求、用户界面需求和资源使用需求。
|
||||||
|
|
||||||
|
\subsection{系统介绍}
|
||||||
|
|
||||||
|
随着互联网技术的发展,越来越多的用户选择在自己的家庭网络中搭建网络附加存储(NAS),作为自己家庭网络上的核心存储设备,并在NAS上下载和存储自己所喜爱的视频和音频资料。同时随着移动终端技术的发展,尤其是4G技术和Wi-Fi技术为终端提供了充足的带宽之后,在移动设备上访问并收听观看家庭NAS中存储的各种资料变成为了一个非常常见的需求。
|
||||||
|
|
||||||
|
本系统便是一个为了解决家庭影音需求而设计的移动端多媒体播放系统。系统应提供两个部分的应用程序,第一部署在NAS上提供影音资料识别、推流的服务端系统,第二是安装在移动终端上的客户端程序,该软件从服务端系统中获取当前NAS中存储各种影音资料的信息并展示给用户选择,提供音乐的播放和视频的观看功能。
|
||||||
|
|
||||||
|
\subsection{系统面向的用户群体}
|
||||||
|
|
||||||
|
本系统面向的用户群体为家庭网络中搭建了NAS的家庭用户群体,尤其是将家庭中的影音资料存储在NAS中并且希望能够在移动终端上查看自己NAS中资料的用户。
|
||||||
|
|
||||||
|
\subsection{系统应当遵守的标准和规范}
|
||||||
|
|
||||||
|
在各种软件系统的开发中,遵循一系列标准和规范是非常重要的,这不仅有助于保证应用的质量,还能增强其安全性、性能和用户体验。
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item 隐私政策和数据保护。遵守GDPR、CCPA等数据保护法规,确保用户数据的合法收集、使用和存储。
|
||||||
|
\item 版权和许可。尊重第三方的知识产权,正确使用开源软件和API。
|
||||||
|
\item 适配性标准。确保应用能在不同设备和操作系统版本上正常运行,包括屏幕尺寸和方向适应性。
|
||||||
|
\item 无障碍标准。设计应用时考虑到残疾用户的需求,遵循WCAG等无障碍指南。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\subsection{系统的功能需求}
|
||||||
|
|
||||||
|
\begin{center}
|
||||||
|
\begin{longtable}{cp{6cm}p{6cm}}
|
||||||
|
\caption{Chiara系统功能需求}
|
||||||
|
\label{tab:function-requirements} \\
|
||||||
|
\toprule
|
||||||
|
\textbf{功能类别} & \textbf{功能名称} & \textbf{描述} \\
|
||||||
|
\endhead
|
||||||
|
媒体仓库 & 设置媒体仓库 & 媒体仓库是对用户文件系统中某一位置下所有媒体文件的统一逻辑抽象。用户可以设置媒体仓库的位置,扫描该路径下的所有媒体文件。\\
|
||||||
|
\midrule
|
||||||
|
\multirow{3}{*}{音视频元信息} & 音频元信息的获取 & 利用元数据的识别工具从扫描到的mp3格式和flac格式的音频文件中提取歌曲名称,歌手,封面图片等的元信息,并存储在数据库中 \\
|
||||||
|
\cmidrule{2-3}
|
||||||
|
& 视频元信息的获取 & 利用元数据识别工具从扫描到的mp4格式和mkv格式的视频文件中提供视频所在节目的名称,视频当前节目中集的编号和标题,利用工具从视频中截取图片作为封面图片进行显示 \\
|
||||||
|
\cmidrule{2-3}
|
||||||
|
& 元信息的存储和刷新 & 在获得到音视频文件的元信息之后,应该将元信息存储在持久化存储中,并且可以在用户的控制或者监控到文件发生变化时自动刷新文件的元信息 \\
|
||||||
|
\midrule
|
||||||
|
\multirow{2}{*}{音视频的播放} & 音频的播放 & 考虑到音频文件的大小一般较小,在播放时可以直接通过HTTP协议以原文件的形式提供。\\
|
||||||
|
\cmidrule{2-3}
|
||||||
|
& 视频的播放 & 考虑到移动端设备的解码能力和传输数据量的问题,需求在播放视频文件时采用流媒体的技术分片进行播放。\\
|
||||||
|
\bottomrule
|
||||||
|
\end{longtable}
|
||||||
|
\end{center}
|
||||||
|
|
||||||
|
\subsection{系统的非功能需求}
|
||||||
|
|
||||||
|
本系统的非功能需求主要为性能需求、环境需求、用户界面需求和资源使用需求。性能需求是系统的技术性能指标,尤其是系统的实时性和其他时间要求。环境需求是系统运行时所处环境的要求。例如在硬件方面应该采用什么机型,需求什么外部接口和数据通信接口;在软件方面应该采用什么支持系统运行的系统软件等。用户界面的基本需求是做到用户界面的友好,使得用户能够方面有效愉快地使用该软件。资源使用需求是指系统运行时所需的数据,软件、内存空间等各项资源。
|
||||||
|
|
||||||
|
\begin{longtable}{cp{6cm}p{6cm}}
|
||||||
|
\caption{Chiara系统非功能需求}
|
||||||
|
\label{tab:non-function-requirements} \\
|
||||||
|
\toprule
|
||||||
|
\textbf{需求类别} & \textbf{需求名称} & \textbf{描述} \\
|
||||||
|
\midrule
|
||||||
|
\endhead
|
||||||
|
\multirow{2}{*}{性能需求} & 快速的元信息扫描 & 扫描元信息的过程应当快速且准确。 \\
|
||||||
|
\cmidrule{2-3}
|
||||||
|
& 快速的视频转码和推流 & 在观看视频时应该按照视频播放的速度将视频转换为移动终端支持的格式并推送出去。 \\
|
||||||
|
\midrule
|
||||||
|
\multirow{2}{*}{环境需求} & 客户端运行环境 & 客户端应该面向最新的Android API版本进行开发,并且支持在Android API 28版本之上的环境中运行。 \\
|
||||||
|
\cmidrule{2-3}
|
||||||
|
& 服务器运行环境 & 服务器程序使用Docker的方式进行交付,并且在x86-64的兼容服务器上运行。\\
|
||||||
|
\midrule
|
||||||
|
用户界面需求 & 友好的用户界面 & 提供易于使用的用户界面,使用设计语言的方式引导用户进行使用,降低用户使用时的学习成本。支持黑暗模式。 \\
|
||||||
|
\midrule
|
||||||
|
\multirow{2}{*}{资源使用需求} & 较小的存储空间使用 & 在进行视频转码时,必然需要将转码之后的文件缓存在文件系统中提供给客户端使用,需要控制缓存文件的大小。\\
|
||||||
|
& 较低的计算资源需求 & 考虑在移动终端的性能问题,在服务器进行视频推流时需要提供符合移动端解码能力的视频流。\\
|
||||||
|
\bottomrule
|
||||||
|
\end{longtable}
|
||||||
|
|
||||||
|
|
||||||
|
\end{document}
|
761
AndroidReport/ch03-design.tex
Normal file
|
@ -0,0 +1,761 @@
|
||||||
|
\documentclass[main.tex]{subfiles}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
\section{系统设计与实现}
|
||||||
|
|
||||||
|
%总体设计、系统组成、各层模块设计, 关键代码的解释
|
||||||
|
|
||||||
|
在本节中将介绍Chiara系统的总体设计和系统实现。在总体设计的环节将介绍系统组成和各层模块的设计,在系统实现的环节将介绍各层模块的实现原理并给出部分关键代码的解释。
|
||||||
|
|
||||||
|
\subsection{系统架构}
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.75\linewidth]{assets/system-architecture.png}
|
||||||
|
\caption{Chiara系统架构图}
|
||||||
|
\label{fig:system-architecture}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
Chiara系统使用服务端/客户端的C/S架构进行开发,其中服务端软件使用ASP.NET core技术进行开发,限于作业的性质,这里不再赘述。客户端软件使用Jetpack Compose框架,按照MVVM设计模式进行开发,使用HTTP协议同后端进行通信。系统的架构如图\ref{fig:system-architecture}所示。
|
||||||
|
|
||||||
|
系统主要由如下几个部分组成:
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item 模型层(Model)。是系统中数据实体对象的逻辑抽象层,在视图对象和服务层交互、服务层和后端服务器交互过程中数据交互的对象。
|
||||||
|
\item 视图层(View)。负责定义用户在屏幕上看到的结构、布局和外观。在理想情况下不包含业务逻辑;但是,在某些情况下,视图层中可能包含一下难以在视图模型层定义的交互逻辑,例如动画。
|
||||||
|
\item 视图模型层(ViewModel)。实现视图可以数据绑定到的属性和命令,并通过更改通知事件通知视图任何状态更改。视图模型提供的属性和命令定义了要由 UI 提供的功能,但视图决定了如何显示该功能。在视图模型层调用服务层的模型时,原则是需要使用异步的调用逻辑以保持UI的响应性。
|
||||||
|
\item 服务层(Service)。实现系统中主要的业务逻辑。在系统中典型的业务逻辑包括使用HTTP RESTful API同后端服务器进行交互;使用跨进程通信使用系统中提供的功能等等。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.8\linewidth]{assets/components.png}
|
||||||
|
\caption{应用组件架构图}
|
||||||
|
\label{fig:components}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
在Android应用系统总体还涉及到应用组件的设计,在Android系统中提供的四种应用组件中,本次系统开发使用到了其中两种,分别是Activity和Service。Activity是与用户交互的入口点,它表示具有界面的单个屏幕。Service是一种通用入口点,用于出于各种原因使应用在后台运行,它是一个在后台运行的组件,用于执行长时间运行的操作或为远程进程执行作业,Service不提供界面。系系统中的应用组件架构如图\ref{fig:components}所示。
|
||||||
|
|
||||||
|
系统中只设计了一个Activity,即Main Activity。在该Activity中设计了用户需要使用的所有界面,包括专辑查看界面,歌曲查看界面,节目查看界面,集查看界面,音乐播放界面和视频播放界面。在系统中还设计了一个Service,即MusicService。设计这个服务是考虑到音视频的编解码是一项较为繁重的工作,如果都放在MainActivity中进行运行可能会造成界面无响应并影响用户体验,同时将音乐和视频的播放放到后台服务中运行可以支持后台播放这一功能。
|
||||||
|
|
||||||
|
\subsection{分层结构设计}
|
||||||
|
|
||||||
|
\subsubsection{视图层}
|
||||||
|
|
||||||
|
视图层是系统中对于应用程序中不同页面的抽象。虽然在系统中只设计了一个Activity,但是在单个活动中仍然存在多个供展示的页面,如果将这些页面都写在同一个函数中,使用各种分支条件判断现在需要显示的内容,这将是软件工程上的灾难。因此,将页面抽象到不同的函数中是十分必要的。
|
||||||
|
|
||||||
|
在将不同的页面编写到函数之后,如何灵活地在不同的页面之间切换便是开发过程中必须要解决的问题。
|
||||||
|
|
||||||
|
在引入了导航库之后,便可以在系统的前端入口函数处定义系统中的导航信息。在定义导航信息的过程中需要给每个页面指定一个唯一的字符串作为路由,在系统的其他部分便可以利用该字符串访问对应的界面。在定义导航信息的同时,还可以定义在导航到该页面中需要携带的数据信息,例如在访问专辑页面时需要携带上一个类型为整数的专辑ID。在表\ref{tab:navigation-definion}中给出了系统中定义的页面。
|
||||||
|
|
||||||
|
\begin{table}[htbp]
|
||||||
|
\centering
|
||||||
|
\begin{tabular}{|c|c|c|}
|
||||||
|
\hline
|
||||||
|
\textbf{页面路由} & \textbf{页面函数} & \textbf{用途} \\
|
||||||
|
\hline
|
||||||
|
splashScreen & SplashPage & 系统的启动页面 \\
|
||||||
|
home & Home & 系统的主页面 \\
|
||||||
|
albumScreen & AlbumPage & 系统中显示某一专辑的页面 \\
|
||||||
|
songScreen & SongPage & 系统中的歌曲播放页面 \\
|
||||||
|
playlistScreen & PlaylistPage & 系统中的播放列表 \\
|
||||||
|
showScreen & ShowPage & 系统中显示某一节目的页面 \\
|
||||||
|
videoScreen & VideoPage & 系统中的视频播放页面 \\
|
||||||
|
\hline
|
||||||
|
\end{tabular}
|
||||||
|
\caption{导航系统的定义}
|
||||||
|
\label{tab:navigation-definion}
|
||||||
|
\end{table}
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
object Navigation {
|
||||||
|
const val SPLASH_SCREEN = "splashScreen"
|
||||||
|
const val HOME = "home"
|
||||||
|
const val ALBUM_SCREEN = "albumScreen"
|
||||||
|
const val SONG_SCREEN = "songScreen"
|
||||||
|
const val PLAYLIST_SCREEN = "playlistScreen"
|
||||||
|
const val SHOW_SCREEN = "showScreen"
|
||||||
|
const val VIDEO_SCREEN = "videoScreen"
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{视图对象层}
|
||||||
|
|
||||||
|
在MVVM架构中,负责给视图层提供数据并处理视图层发生事件的视图模型层是系统中封装主要交互逻辑的一层。在系统中,视图对象由Hilt依赖注入系统进行创建,因此可以在视图对象中访问系统的服务。同时视图对象和视图并不是一对一的关系,在系统中存在一些顶层的视图对象,例如控制播放器状态的视图对象,在\texttt{HomeBottomBar}和\texttt{SongPage}等多个组件中都会使用到。
|
||||||
|
|
||||||
|
\paragraph{MainViewModel}
|
||||||
|
|
||||||
|
系统中的顶层ViewModel,在应用初始化时便完成初始化。在该对象中,提供了当前播放器的状态\texttt{musicControllerState}和当前播放列表中的内容\texttt{playList},还提供了在系统退出时销毁播放器对象和刷新当前播放列表的功能。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
@HiltViewModel
|
||||||
|
class MainViewModel @Inject constructor(
|
||||||
|
val musicController: MusicController
|
||||||
|
) : ViewModel() {
|
||||||
|
var musicControllerState by mutableStateOf(MusicControllerState())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var playList: List<SongResponse> by mutableStateOf(emptyList())
|
||||||
|
private set
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\paragraph{HomePageViewModel}
|
||||||
|
|
||||||
|
主页对应的ViewModel。在该对象中,提供了主页的状态\texttt{homePageState}
|
||||||
|
,还提供了切换视频和音频页面、获取远程数据等的功能。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
@HiltViewModel
|
||||||
|
class AlbumPageViewModel @Inject constructor(
|
||||||
|
apiClient: ApiClient,
|
||||||
|
private val musicController: MusicController
|
||||||
|
) : ViewModel() {
|
||||||
|
var albumPageState by mutableStateOf(AlbumPageState())
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val albumApi = apiClient.createService(AlbumApi::class.java)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\paragraph{AlbumPageViewModel}
|
||||||
|
|
||||||
|
专辑页面对应的ViewModel。该对象提供了专辑页面的状态\texttt{albumPageState},还提供了获得专辑信息\texttt{fetchAlbum}和选择歌曲、播放功能等一系列的功能。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
@HiltViewModel
|
||||||
|
class HomePageViewModel @Inject constructor(
|
||||||
|
private val apiClient: ApiClient
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
var homePageState by mutableStateOf(HomePageState())
|
||||||
|
private set
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\paragraph{ShowPageViewModel}
|
||||||
|
|
||||||
|
节目页面对应的ViewModel。在该对象中提供了节目页面的状态\texttt{showPageState},还提供获得节目信息的功能\texttt{fetchShowSeason}。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
@HiltViewModel
|
||||||
|
class ShowPageViewModel @Inject constructor(
|
||||||
|
apiClient: ApiClient
|
||||||
|
) : ViewModel() {
|
||||||
|
private val showSeasonApi = apiClient.createService(SeasonApi::class.java)
|
||||||
|
|
||||||
|
var showPageState by mutableStateOf(ShowPageState())
|
||||||
|
private set
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\paragraph{VideoPageViewModel}
|
||||||
|
|
||||||
|
视频播放页对应的ViewModel。在该对象中提供了视频播放页面的状态\texttt{videoPageState}和显示提示信息栏的状态\texttt{snackBarHostState},还提供了获得视频信息并开发播放的功能。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
@HiltViewModel
|
||||||
|
class VideoPageViewModel @Inject constructor(
|
||||||
|
val musicController: MusicController,
|
||||||
|
apiClient: ApiClient
|
||||||
|
) : ViewModel() {
|
||||||
|
private val hlsApi = apiClient.createService(HlsApi::class.java)
|
||||||
|
|
||||||
|
var videoPageState by mutableStateOf(VideoPageState())
|
||||||
|
private set
|
||||||
|
|
||||||
|
val snackBarHostState = SnackbarHostState()
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsubsection{模型层}
|
||||||
|
|
||||||
|
在系统中主要存在两种对象,第一是使用OpenApiGenerator生成的,用于在服务端和客户端通信时传递信息的对象。第二是表示每个页面状态的对象。因此这里只简述第一种类,重点介绍第二种模型类。
|
||||||
|
|
||||||
|
\paragraph{专辑(Album)} 专辑是系统中对于一系列音乐的抽象。专辑中有专辑ID、专辑标题、艺术家、封面图片地址和专辑内歌曲列表等几个字段。
|
||||||
|
|
||||||
|
\paragraph{歌曲(Song)} 歌曲是系统中对于一首音乐的抽象。歌曲中有歌曲ID,歌曲标题、艺术家、封面图片地址、歌曲文件地址等几个字段。
|
||||||
|
|
||||||
|
\paragraph{节目季(ShowSeason)} 节目季是系统中对于一系列音乐的抽象。节目季中有节目ID、节目名称和节目内集列表等几个字段。
|
||||||
|
|
||||||
|
\paragraph{集(Episode)} 集是系统中对于一个视频的抽象。集中有集ID、集标题、集标号等几个字段。
|
||||||
|
|
||||||
|
|
||||||
|
\paragraph{HomePageState}
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
data class HomePageState(
|
||||||
|
val loading: Boolean = false,
|
||||||
|
val isMusicPage: Boolean = true,
|
||||||
|
val selectedAlbum : AlbumResponse? = null,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
val albums: List<AlbumResponse> = emptyList(),
|
||||||
|
val selectedSeason : ShowSeasonResponse? = null,
|
||||||
|
val seasons: List<ShowSeasonResponse> = emptyList(),
|
||||||
|
)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
HomePageState数据类用于表示应用首页的状态。
|
||||||
|
在该类中声明一个布尔类型的属性loading,用于指示当前页面是否正在加载数据,默认值为false。
|
||||||
|
声明一个布尔类型的属性isMusicPage,用于判断当前页面是否是音乐页面,默认值为true。
|
||||||
|
声明一个可空的AlbumResponse类型的属性selectedAlbum,用于存储用户选择的专辑信息,默认值为null。
|
||||||
|
声明一个可空的字符串类型属性errorMessage,用于存储错误消息,默认值为null。
|
||||||
|
声明一个AlbumResponse类型的列表属性albums,用于存储所有专辑信息,默认值为一个空列表。
|
||||||
|
声明一个可空的ShowSeasonResponse类型的属性selectedSeason,用于存储用户选择的季信息,默认值为null。
|
||||||
|
声明一个ShowSeasonResponse类型的列表属性seasons,用于存储所有季信息,默认值为一个空列表。
|
||||||
|
|
||||||
|
HomePageState类是为了管理系统中的首页状态,包括加载状态、是否显示音乐页面、选中的专辑、专辑列表、选中的季以及季列表等信息。
|
||||||
|
|
||||||
|
\paragraph{MusicControllerState}
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
data class MusicControllerState(
|
||||||
|
val playerState: PlayerState= PlayerState.STOPPED,
|
||||||
|
val currentSong: SongResponse? = null,
|
||||||
|
val currentPosition: Long = 0L,
|
||||||
|
val totalDuration: Long = 0L,
|
||||||
|
val repeatState : RepeatState = RepeatState.Sequence
|
||||||
|
)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
MusicControllerState数据类,用于表示音乐播放控制器的状态。
|
||||||
|
playerState表示当前播放器的状态,默认为STOPPED。PlayerState应该是一个枚举类型,定义了播放器可能的各种状态(例如:STOPPED, PLAYING, PAUSED等)。
|
||||||
|
currentSong表示当前正在播放的歌曲信息,类型为SongResponse,这是一个可空类型,意味着在没有歌曲播放时该值可以为null。
|
||||||
|
currentPosition:表示当前播放位置的时间戳,单位是毫秒,默认值为0L。
|
||||||
|
totalDuration表示当前歌曲的总时长,同样以毫秒为单位,默认值为0L。
|
||||||
|
repeatState表示重复模式的状态,默认为Sequence。RepeatState应该也是一个枚举类型,定义了播放器的重复模式(例如:Sequence,RepeatOne,RepeatAll等)。
|
||||||
|
|
||||||
|
通过这个MusicControllerState类可以轻松地跟踪和控制音乐播放器的当前状态,包括播放状态、当前歌曲、播放位置、歌曲总时长以及重复模式等关键信息。
|
||||||
|
|
||||||
|
\paragraph{AlbumPageState}
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
data class AlbumPageState(
|
||||||
|
val loading: Boolean = false,
|
||||||
|
val selectedSong: SongResponse? = null,
|
||||||
|
val album: AlbumResponse? = null,
|
||||||
|
val errorMessage: String? = null
|
||||||
|
)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
AlbumPageState数据类用于封装和管理与专辑页面相关的状态信息。
|
||||||
|
loading是一个布尔类型的字段,表示页面是否正在加载数据,默认值为false。
|
||||||
|
selectedSong是一个可空的SongResponse类型的字段,代表当前选中的歌曲信息,默认值为null。
|
||||||
|
album是一个可空的AlbumResponse类型的字段,代表当前显示的专辑信息,默认值为null。
|
||||||
|
errorMessage是一个可空的字符串类型的字段,用于存储在加载数据过程中可能出现的错误信息,默认值为null。
|
||||||
|
综上所述,AlbumPageState类用于存储专辑页面的关键状态,包括加载状态、选中的歌曲、当前专辑的信息以及任何可能发生的错误信息。
|
||||||
|
|
||||||
|
\paragraph{ShowPageState}
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
data class ShowPageState(
|
||||||
|
val loading: Boolean = false,
|
||||||
|
val showSeason: ShowSeasonResponse? = null
|
||||||
|
)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
ShowPageState数据类包含两个字段。
|
||||||
|
loading是一个布尔型字段,表示当前页面是否处于加载状态。默认情况下,此字段被初始化为false,意味着页面加载尚未开始或已加载完成。
|
||||||
|
showSeason:是一个可空的ShowSeasonResponse类型的字段,用于存储当前显示的节目季信息。默认值为null,这可能意味着在页面加载之前或加载失败的情况下,该字段不会包含有效数据。
|
||||||
|
ShowPageState数据类用于表示与特定节目季相关的页面状态,
|
||||||
|
|
||||||
|
\paragraph{VideoPageState}
|
||||||
|
|
||||||
|
\begin{lstlisting}
|
||||||
|
data class VideoPageState(
|
||||||
|
val loading: Boolean = false,
|
||||||
|
val playing: Boolean = false,
|
||||||
|
)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
VideoPageState数据类包含了两个布尔型字段。
|
||||||
|
loading表示视频页面当前是否正在加载内容,默认值为false,意味着页面加载已完成或尚未开始加载。
|
||||||
|
playing表示视频当前是否正在播放,默认值为false,意味着视频未播放或暂停状态。
|
||||||
|
VideoPageState数据类可以用来表示视频页面的状态。
|
||||||
|
|
||||||
|
\subsubsection{服务层}
|
||||||
|
|
||||||
|
系统中服务层中主要的服务为提供Exoplayer的MusicService和与之进行跨进程通信,位于MainActivity之类的MusicController以及同后端服务器进行通信的ApiClient。在上面中ApiClient是通过OpenApiGenerator生成的模板代码,封装了okhttp3的HTTP请求功能和kotlinx-serialization的JSON序列化功能,这里就不做详述。下面主要介绍MusicService和MusicController\footnote{这里虽然变量命名为Music,实际上同时负责了视频和音频的播放功能}。
|
||||||
|
|
||||||
|
\paragraph{MusicController}
|
||||||
|
|
||||||
|
MusicController使用接口的软件工程方法进行设计,分为MusicController接口和MusicControllerImpl类,后一个类实现前一个接口。这里重点介绍接口中的回调和方法。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
var mediaControllerCallback: (
|
||||||
|
(
|
||||||
|
playerState: PlayerState,
|
||||||
|
currentMusic: SongResponse?,
|
||||||
|
currentPosition: Long,
|
||||||
|
totalDuration: Long,
|
||||||
|
repeatState: RepeatState
|
||||||
|
) -> Unit
|
||||||
|
)?
|
||||||
|
|
||||||
|
var mediaControllerReadyCallback: ((Player) -> Unit)?
|
||||||
|
|
||||||
|
var mediaControllerErrorCallback: ((PlaybackException) -> Unit)?
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
在MusicController中首先定义了三个回调函数,第一个回调函数负责更新当前播放器的状态,包括是否正在播放,正在播放的歌曲、正在播放的位置、 当前歌曲的总长度和播放列表的重复状态。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
var player: Player?
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
在MusicController中定义了一个可以获得Player接口的字段,该类型在界面上绘制视频播放界面时会用到。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
/**
|
||||||
|
* 添加一首曲目到播放列表中
|
||||||
|
*/
|
||||||
|
fun addSong(song: SongResponse)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加一系列曲目到播放列表中
|
||||||
|
*/
|
||||||
|
fun addSongRange(songs: Iterable<SongResponse>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空播放列表
|
||||||
|
*/
|
||||||
|
fun clearPlayList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除播放列表中的指定曲目
|
||||||
|
*/
|
||||||
|
fun remove(songId: Int)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
在MusicController中定义一系列同播放列表进行交互的功能。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
/**
|
||||||
|
* 播放播放列表中的指定曲目
|
||||||
|
*/
|
||||||
|
fun play(songId: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复播放
|
||||||
|
*/
|
||||||
|
fun resume()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停
|
||||||
|
*/
|
||||||
|
fun pause()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得当前播放歌曲的位置
|
||||||
|
*/
|
||||||
|
fun getCurrentPosition(): Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到下一首曲目
|
||||||
|
*/
|
||||||
|
fun skipToNextSong()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到上一首曲目
|
||||||
|
*/
|
||||||
|
fun skipToPreviousSong()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得当前正在播放的曲目
|
||||||
|
*/
|
||||||
|
fun getCurrentSong(): SongResponse?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到指定位置
|
||||||
|
*/
|
||||||
|
fun seekTo(position: Long)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
MusicController定义了和播放音乐相关的一系列功能。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
fun setSequenceRepeat()
|
||||||
|
|
||||||
|
fun setRepeatOne()
|
||||||
|
|
||||||
|
fun setShuffleRepeat()
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
|
||||||
|
MusicController定义了设置播放列表重复逻辑的功能,包括设置顺序播放、单曲循环和随机播放。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
fun playVideo(mediaItem: MediaItem)
|
||||||
|
|
||||||
|
fun stopVideo()
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
MusicController定义了播放视频和停止播放视频的功能。
|
||||||
|
|
||||||
|
\paragraph{MusicService}
|
||||||
|
|
||||||
|
MusicService是为实现视频和音频的后端播放,将Exoplayer对象托管给后端服务而设计了。该MusicService继承了MediaSessionService,并在类中重写了\texttt{onCreate}和\texttt{onDestroy}两个方法,在\texttt{onCreate}方法中将从依赖注入的系统中获得Exoplayer类实例,并利用该实例构建\texttt{MediaSession},这是Media3跨进程通信中的核心组件。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class MusicService : MediaSessionService() {
|
||||||
|
private var mediaSession: MediaSession? = null
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var exoPlayer: ExoPlayer
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
mediaSession = MediaSession.Builder(this, exoPlayer)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
||||||
|
return mediaSession
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
mediaSession?.run {
|
||||||
|
player.release()
|
||||||
|
release()
|
||||||
|
mediaSession = null
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsection{关键代码段}
|
||||||
|
|
||||||
|
在本节中将重点关注系统中重点功能的核心实现细节。
|
||||||
|
|
||||||
|
\paragraph{在协程中更新播放器的状态}
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
init {
|
||||||
|
musicController.mediaControllerCallback = { playerState,
|
||||||
|
currentMusic,
|
||||||
|
currentPosition,
|
||||||
|
totalDuration,
|
||||||
|
repeatState ->
|
||||||
|
musicControllerState = musicControllerState.copy(
|
||||||
|
playerState = playerState,
|
||||||
|
currentSong = currentMusic,
|
||||||
|
currentPosition = currentPosition,
|
||||||
|
totalDuration = totalDuration,
|
||||||
|
repeatState = repeatState
|
||||||
|
)
|
||||||
|
|
||||||
|
if (playerState == PlayerState.PLAYING) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
while (true) {
|
||||||
|
// 每秒钟更新一次播放器的状态
|
||||||
|
delay(1.seconds)
|
||||||
|
musicControllerState = musicControllerState.copy(
|
||||||
|
currentPosition = musicController.getCurrentPosition()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
这段Kotlin代码是在初始化MainViewModel内设置的一个媒体控制器回调,主要负责更新musicControllerState状态。
|
||||||
|
在代码中设置musicController的回调函数,当播放器状态、当前播放的音乐、播放位置、总时长或重复状态发生变化时,会调用这个匿名函数。函数参数:playerState, currentMusic, currentPosition, totalDuration, repeatState分别代表播放器状态、当前播放的音乐、当前播放位置、音乐总时长和重复模式。
|
||||||
|
在回调函数内部,首先通过\texttt{copy}方法创建一个新的MusicControllerState实例,将新的播放器状态、当前播放的音乐、播放位置、总时长和重复模式赋值给新实例的对应字段,然后将musicControllerState更新为这个新实例。这样做的目的是保持状态的不可变性,避免直接修改现有状态实例。
|
||||||
|
如果playerState为PlayerState.PLAYING(即播放器正在播放),则启动一个新的协程viewModelScope.launch,在这个协程中执行一个无限循环,每秒钟更新一次播放器的当前播放位置。具体来说,它会调用delay(1.seconds)让协程暂停一秒,然后通过musicController.getCurrentPosition()获取当前播放位置,并再次更新musicControllerState的currentPosition字段。
|
||||||
|
|
||||||
|
这段代码的核心在于如何处理播放器状态的实时更新,特别是利用Kotlin协程来实现异步和非阻塞的操作,确保UI的流畅性和响应性。同时,通过不可变数据结构(copy方法)来更新状态,有助于简化状态管理和避免潜在的并发问题。
|
||||||
|
|
||||||
|
\paragraph{依赖注入代码}
|
||||||
|
|
||||||
|
系统中的依赖注入是系统中实现服务与视图模型等各个类之间解耦的关键,下面说明系统中负责定义依赖注入的关键代码。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object AppModule {
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideApiClient() = ApiClient(
|
||||||
|
baseUrl = Configuration.SERVER_ADDRESS,
|
||||||
|
okHttpClientBuilder = OkHttpClient.Builder().readTimeout(Duration.ofSeconds(30))
|
||||||
|
)
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideMusicController(@ApplicationContext context: Context): MusicController =
|
||||||
|
MusicControllerImpl(context)
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
这段Kotlin代码定义了一个Dagger模块(AppModule),它用于提供应用程序中需要的依赖项。
|
||||||
|
\texttt{@InstallIn(SingletonComponent::class)}注解指定了当前模块提供的依赖项应该被安装到哪种类型的Dagger组件中。在这里,依赖项将被安装SingletonComponent中,意味着它们将作为单例在整个应用程序的生命周期中共享。
|
||||||
|
\texttt{@Singleton}注解表明由provideApiClient和provideMusicController函数提供的依赖项应该被创建一次并在整个应用程序中复用,即作为单例存在。
|
||||||
|
\texttt{@Provides}注解表示下面的函数是用来提供依赖项的,告诉Dagger如何实例化这些依赖项。
|
||||||
|
|
||||||
|
\texttt{provideApiClient}这个函数提供了ApiClient的实例。ApiClient的构造函数接受两个参数:baseUrl和okHttpClientBuilder。这里使用了Configuration.SERVER\_ADDRESS作为基础URL,而OkHttpClient.Builder()用于构建HTTP客户端,设置了读取超时时间为30秒。
|
||||||
|
\texttt{provideMusicController}函数提供了MusicController的实例。通过将Context作为参数传递给MusicControllerImpl的构造函数,它返回了一个MusicControllerImpl的实例,这是MusicController接口的一个实现。注意,@ApplicationContext注解表明传递的Context应该是应用程序级别的上下文,这对于访问全局资源或执行某些操作(如注册广播接收器)非常重要。
|
||||||
|
|
||||||
|
总的来说,这段代码负责配置和提供应用程序中需要的网络API客户端和音乐控制服务。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
@Module
|
||||||
|
@InstallIn(ServiceComponent::class)
|
||||||
|
object ServiceModule {
|
||||||
|
@ServiceScoped
|
||||||
|
@Provides
|
||||||
|
fun provideAudioAttributes() = AudioAttributes.Builder()
|
||||||
|
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||||
|
.setUsage(C.USAGE_MEDIA)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
@ServiceScoped
|
||||||
|
@Provides
|
||||||
|
fun provideExoPlayer(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
autoAttributes: AudioAttributes
|
||||||
|
) = ExoPlayer.Builder(context).build().apply {
|
||||||
|
setAudioAttributes(autoAttributes, true)
|
||||||
|
setHandleAudioBecomingNoisy(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
这段Kotlin代码同样是在使用Dagger框架进行依赖注入,主要为多媒体服务组件(ServiceComponent)提供必要的依赖。
|
||||||
|
\texttt{@InstallIn(ServiceComponent::class)}指定该模块的依赖项应被安装在ServiceComponent中。这意味着该模块提供的依赖将被服务组件所使用,这通常适用于多媒体应用中的后台服务或播放器服务。
|
||||||
|
\texttt{ServiceScoped}注解类似于\texttt{@Singleton},但它表示这些依赖项的生命周期将与服务组件的生命周期绑定,而不是整个应用。这在处理多媒体服务时很重要,因为服务可能在用户不交互应用时仍然运行,因此其依赖项不应在整个应用的生命周期内保持活动状态。
|
||||||
|
|
||||||
|
\texttt{provideAudioAttributes}方法提供了一个AudioAttributes的实例,它定义了音频流的属性,如内容类型和使用场景。在这里,AudioAttributes被设置为音乐类型,并且用于媒体播放。这对于控制音频流如何与其他系统音频流交互非常重要。
|
||||||
|
\texttt{provideExoPlayer}方法提供了一个ExoPlayer实例,该方法接收应用程序上下文和前面提供的AudioAttributes,然后创建并配置ExoPlayer实例。它设置了音频属性,并指示播放器在音频可能变得嘈杂时采取行动,例如当用户接电话或有其他系统声音时。
|
||||||
|
|
||||||
|
总体而言,这段代码负责为多媒体服务组件提供必要的音频属性和播放器实例。通过使用Dagger的依赖注入,它可以确保这些服务组件能够获得正确配置的依赖项,从而使得多媒体服务能够正常运行,同时保持代码的清晰和可测试性。
|
||||||
|
|
||||||
|
\paragraph{请求服务端数据}
|
||||||
|
|
||||||
|
在客户端中存在多次发起HTTP请求服务端的数据,这里选择一处获得专辑数据作为示例介绍如何完成数据的请求。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
fun fetchAlbum(albumId: Int) {
|
||||||
|
albumPageState = albumPageState.copy(
|
||||||
|
loading = true
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
val albumResponse = albumApi.apiAlbumIdGet(albumId)
|
||||||
|
var album = albumResponse.body()
|
||||||
|
|
||||||
|
if (albumResponse.isSuccessful && album != null) {
|
||||||
|
album = album.copy(
|
||||||
|
coverImageUrl = Configuration.SERVER_ADDRESS + album.coverImageUrl,
|
||||||
|
songs = album.songs.map {
|
||||||
|
it.copy(
|
||||||
|
coverImageUrl = Configuration.SERVER_ADDRESS + it.coverImageUrl,
|
||||||
|
url = Configuration.SERVER_ADDRESS + it.url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
albumPageState = albumPageState.copy(
|
||||||
|
loading = false,
|
||||||
|
album = album
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
albumPageState = albumPageState.copy(
|
||||||
|
loading = false,
|
||||||
|
errorMessage = "网络错误"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
这段Kotlin代码展示了一个函数fetchAlbum,其功能是从服务器获取特定ID的专辑信息,并更新UI状态以反映加载进度和结果。
|
||||||
|
在函数开始处,首先更新albumPageState的loading字段为true,这用于显示UI上的加载指示器。
|
||||||
|
使用viewModelScope.launch发起异步请求,这允许在后台线程上执行网络调用,而不会阻塞主线程,保证了UI的响应性。
|
||||||
|
调用albumApi.apiAlbumIdGet(albumId)执行HTTP GET请求,从服务器获取专辑详情。
|
||||||
|
使用body()方法解析响应体,尝试将其转换为Album对象。
|
||||||
|
如果请求成功(albumResponse.isSuccessful)且响应体不为空(album != null):对Album对象进行一些额外的处理,如拼接完整的图片URL和歌曲URL,这是因为服务器返回的是相对路径,需要加上服务器地址才能构成完整的URL。将处理后的Album对象存储在albumPageState中,并将loading字段设回false,表示数据加载完成。如果请求失败或响应体为空:将loading字段设为false,并设置一个错误消息errorMessage = "网络错误",这将用于向用户显示错误信息。
|
||||||
|
|
||||||
|
此函数实现了从服务器获取专辑数据的基本流程,包括启动加载状态、异步网络请求、数据处理以及UI状态更新。它还妥善处理了网络请求的错误情况,确保了用户体验的连贯性和应用程序的健壮性。使用viewModelScope和协程使得异步操作更加简洁和高效,避免了回调地狱或复杂的同步逻辑。
|
||||||
|
|
||||||
|
\paragraph{启动页面时完成初始化的工作}
|
||||||
|
|
||||||
|
在系统中进入不少页面时都需要完成初始化的工作,例如从服务段请求数据。显然,在系统中并不用在每次进入该页面时均执行一个初始化的工作,这样只会造成不必要的网络开销,降低用户使用应用的体验。下面选择进入进入专辑页面时的初始化逻辑介绍,如何只在第一次打开页面时执行一次网络请求。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
composable(
|
||||||
|
"${Navigation.ALBUM_SCREEN}/{albumId}",
|
||||||
|
arguments = listOf(navArgument("albumId") {
|
||||||
|
type = NavType.IntType
|
||||||
|
})
|
||||||
|
) { backStackEntry ->
|
||||||
|
val albumId = backStackEntry.arguments?.getInt("albumId")
|
||||||
|
val albumPageViewModel: AlbumPageViewModel = hiltViewModel()
|
||||||
|
val isInitialized = rememberSaveable(stateSaver = autoSaver()) {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
if (!isInitialized.value) {
|
||||||
|
LaunchedEffect(key1 = Unit) {
|
||||||
|
albumId?.let {
|
||||||
|
albumPageViewModel.fetchAlbum(it)
|
||||||
|
}
|
||||||
|
isInitialized.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlbumPage(
|
||||||
|
mainViewModel = mainViewModel,
|
||||||
|
albumPageViewModel = albumPageViewModel,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
这段代码是使用Jetpack Compose和Hilt依赖注入框架编写的一个Composable函数,用于展示专辑页面。composable函数定义了一个名为ALBUM\_SCREEN的导航目标,其路径包含一个动态参数{albumId},用于接收从上一个屏幕传来的专辑ID。然后从backStackEntry中提取albumId参数。如果参数存在,getInt("albumId")将返回对应的整数值;如果不存在,则返回null。
|
||||||
|
|
||||||
|
使用Hilt框架注入AlbumPageViewModel实例。使用rememberSaveable创建一个记忆化的状态isInitialized,初始值为false。autoSaver()自动保存和恢复状态,确保在设备配置改变(如旋转屏幕)时,状态能够正确保留。
|
||||||
|
|
||||||
|
检查isInitialized状态,如果为false,则触发数据加载过程。使用LaunchedEffect在组件初次渲染或重新创建时执行一次性的副作用,这里用于加载数据。如果albumId非空,调用fetchAlbum函数加载专辑数据。数据加载完成后,将isInitialized设为true,防止重复加载。最终调用AlbumPage Composable,传入所需的ViewModel和NavController,以显示专辑页面的内容。
|
||||||
|
|
||||||
|
此代码片段展示了如何在Jetpack Compose中定义和处理导航目标,使用Hilt注入ViewModel,以及如何在页面加载时延迟加载数据。这种模式确保了数据加载的效率和UI的响应性,同时利用Hilt和Compose的特性简化了代码结构。
|
||||||
|
|
||||||
|
\paragraph{播放动画的实现}
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.4\linewidth]{assets/song-playing.png}
|
||||||
|
\caption{播放歌曲的动画}
|
||||||
|
\label{fig:song-playing}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
如图\ref{fig:song-playing}所示,在系统中设计了一个播放歌曲时的动画,下面的代码段解释了如何在程序中实现该动画效果。
|
||||||
|
|
||||||
|
\begin{lstlisting}[language=Kotlin]
|
||||||
|
@Composable
|
||||||
|
fun AnimatedVinyl(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isSongPlaying: Boolean = false,
|
||||||
|
painter: Painter
|
||||||
|
) {
|
||||||
|
var currentRotation by remember {
|
||||||
|
mutableFloatStateOf(0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
val rotation = remember {
|
||||||
|
Animatable(currentRotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(isSongPlaying) {
|
||||||
|
if (isSongPlaying) {
|
||||||
|
rotation.animateTo(
|
||||||
|
targetValue = currentRotation + 360f, animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(3000, easing = LinearEasing),
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
currentRotation = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (currentRotation > 0f) {
|
||||||
|
rotation.animateTo(
|
||||||
|
targetValue = currentRotation + 50f, animationSpec = tween(
|
||||||
|
1250, easing = LinearEasing
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
currentRotation = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vinyl(modifier = modifier, painter = painter, rotationDegrees = rotation.value)
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
这段代码定义了一个名为AnimatedVinyl的Jetpack Compose Composable函数,用于展示一个动画化的黑胶唱片界面,其行为和外观随歌曲播放状态变化。函数的参数有: modifier:允许外部传入一个Modifier,用于自定义布局和样式。isSongPlaying:一个布尔值,指示歌曲是否正在播放,影响动画行为。painter:一个Painter对象,用于绘制黑胶唱片的图像。
|
||||||
|
|
||||||
|
在代码中使用remember创建一个记忆化的浮点型状态currentRotation,用于存储当前的旋转角度。创建一个Animatable对象,用于动画的平滑过渡,初始值为currentRotation。当isSongPlaying的值发生变化时,触发动画逻辑的执行。如果歌曲开始播放,启动无限循环的动画,使唱片连续旋转360度。其中动画的规格为动画持续时间3秒,采用线性缓动,无限循环重启。
|
||||||
|
如果歌曲停止播放,动画停止,并将唱片旋转至稍微超过起始位置的角度,以模拟惯性效果。
|
||||||
|
最后调用Vinyl Composable,传入必要的参数,包括modifier、painter和当前的旋转角度,以绘制旋转的黑胶唱片图像。
|
||||||
|
|
||||||
|
AnimatedVinyl通过结合remember、Animatable和LaunchedEffect,实现了基于歌曲播放状态的动态黑胶唱片动画。当歌曲播放时,唱片连续旋转;当歌曲暂停或停止时,动画平滑减速并停止。这种动画不仅增强了用户体验,还有效地反映了歌曲播放的状态,使得UI更加生动和直观。
|
||||||
|
|
||||||
|
\subsection{系统界面设计}
|
||||||
|
|
||||||
|
在本节中将主要介绍系统中各个页面的设计逻辑和交互方式。
|
||||||
|
|
||||||
|
图\ref{fig:main-page}展示了系统中的主页面。主页面按照典型的移动应用布局进行设计,分成顶部栏、中间内容和底部栏三个部分。
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\begin{subfigure}{0.45\linewidth}
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.9\linewidth]{assets/main-music-page.png}
|
||||||
|
\caption{展示音乐列表时}
|
||||||
|
\end{subfigure}
|
||||||
|
\begin{subfigure}{0.45\linewidth}
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.9\linewidth]{assets/main-video-page.png}
|
||||||
|
\caption{展示视频列表时}
|
||||||
|
\end{subfigure}
|
||||||
|
\caption{主页}
|
||||||
|
\label{fig:main-page}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
顶部栏展示了当前应用的名称和一个供用户手动刷新服务端数据的按钮,在按下按钮之后系统会再次从服务端中获取音乐和视频的数据。中间部分是一个列表,展示了当前系统中所有的音乐专辑或者视频列表。如果当前为显示专辑,则在每一栏中,展示了每个专辑的标题,艺术家和专辑的封面,其中针对没有扫描或者识别到封面的专业,系统提供了一个默认的封面图片;如果当前是视频列表,当前则会展示每个节目的名称。页面的底部栏由两个部分组成,第一个部分是展示当前音乐播放转状态的播放栏,提供了快捷播放和暂停的切换按钮和进入播放列表页面的按钮。第二部分是切换当前显示系统中的专辑列表或者是视频列表。在进入视频列表页面之后,播放栏会被自动隐藏。
|
||||||
|
|
||||||
|
在点击任意专辑之后,会进入专辑内容的展示页面,如图\ref{fig:album-page}所示。
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.4\linewidth]{assets/album-page.png}
|
||||||
|
\caption{专辑页面}
|
||||||
|
\label{fig:album-page}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
专辑页面在页面框架上保持和主页的一致性,仍然由三个部分组成。标题栏负责显示当前专辑的名称和一个返回上一页的按钮。内容部分是一个展示当前专辑内所有音乐的列表,列表中每一栏展示的信息为每首歌曲的名称、该歌曲的歌手和一张该歌曲的图片。底部栏则只有一个展示当前播放状态的播放栏。
|
||||||
|
|
||||||
|
在页面上不论是点击歌曲还是点击播放状态栏都会进入到歌曲播放的页面。歌曲播放的页面如图所示。
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.4\linewidth]{assets/song-page.png}
|
||||||
|
\caption{歌曲播放界面}
|
||||||
|
\label{fig:song-page}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
歌曲播放界面专注于展示同歌曲播放相关的信息。左上角提供了一个返回上一界面的按钮,可以返回到之前的界面。中间是一张模拟黑胶唱片的动画,会在音乐播放时转动,在暂停音乐播放时停止。接下来的两行字展示了当前正在播放的音乐名称和歌手。最后便是两排控制音乐播放的功能按钮,第一行从左到右是上一首歌曲,播放暂停切换按钮和下一首歌曲按钮,第二行从左到右是切换播放模式按钮和进入播放页面按钮。其中按下切换播放模式按钮会在播放器的三种播放模式之间循环,依次为顺序播放模式、单曲循环播放模式和随机播放模式,随着播放模式的变化,界面上显示的按钮图标也会发生对应的变化。
|
||||||
|
|
||||||
|
点击在歌曲播放页面的播放列表按钮和播放状态栏上的播放列表按钮都会进入到的播放列表页面中。播放列表页面如图\ref{fig:playlist-page}所示。
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.4\linewidth]{assets/playlist-page.png}
|
||||||
|
\caption{播放列表页面}
|
||||||
|
\label{fig:playlist-page}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
播放列表页面仍然保持和整个系统类似的布局风格,仍然分成上中下三个部分。标题栏显示了当前页面的标题和一个返回上一页面的按钮,中间的内容列表展示了当前播放列表中的所有歌曲,其中会高亮显示当前正在播放的歌曲,每首歌曲展示的信息和专辑页面相同。最下面的状态栏为当前正在播放歌曲的状态栏。在页面的右下角提供了一个清空当前播放列表的按钮,在按下该按钮之后就会停止当前正在播放的音乐并清空当前播放器中的播放列表。
|
||||||
|
|
||||||
|
在主页中展示视频列表时点击视频列表中的任意一个节目,将会进入到节目展示页面。节目展示页面如图\ref{fig:show-page}所示。
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.4\linewidth]{assets/show-page.png}
|
||||||
|
\caption{节目展示页面}
|
||||||
|
\label{fig:show-page}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
节目展示页面也保持系统中一贯的页面设计风格,页面主要分成上下两个部分,缺少当前的播放状态。在页面上方的标题栏中,标题栏中展示了当前选择的节目标题。在页面中的列表中展示了当前选择节目中的视频列表,在列表中的每一项中展示了当前视频的标题和当前视频在该节目中的集数。在视频播放页面中没有设计同音乐播放中类似的播放状态栏是因为,视频的播放和音频的播放有着较大的不同,从逻辑上来说,视频播放很少有不需要看画面的时候,设计视频的后台播放意义不是很大,从实现上来说,后台播放视频和音乐在实现并没有任何的不同,不需要在视频的页面上重复的编写对应的代码。因此在这里我们会在进入视频播放页面时自动隐藏播放状态栏。
|
||||||
|
|
||||||
|
在节目展示页面中的点击任意视频就会进入到视频播放页面。视频播放页面如图所示。
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.4\linewidth]{assets/video-page.png}
|
||||||
|
\caption{视频播放页面}
|
||||||
|
\label{fig:video-page}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
视频播放页面在总体上也有三部分组成。最上面的仍然是标题栏,在标题栏中显示了当前播放视频的标题和一个返回上一页面的按钮。在页面的内容部分则分成两个部分,分别是视频的画面部分和接下来推荐观看的视频列表。接下来推荐观看的视频列表所展示的信息和节目页面所展示的信息是一致的,都是视频的标题和集数。
|
||||||
|
|
||||||
|
\end{document}
|
141
AndroidReport/ch04-server.tex
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
\documentclass[main.tex]{subfiles}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
\section{服务端系统设计}
|
||||||
|
|
||||||
|
虽然本课程的主要教学重点是移动互联网技术和移动互联网应用开发的,但是对于一个在线多媒体播放系统而言,如果没有服务端的设计和实现,移动端的应用程序只会是无根之木和无水之萍。因此在本节中,我们将就服务端实现中的总体架构和重点逻辑进行阐述,供参考。
|
||||||
|
|
||||||
|
\subsection{服务端总体架构}
|
||||||
|
|
||||||
|
本系统的服务端使用ASP.NET Core进行开发,在开发的过程中使用典型的MVC架构进行开发,系统的架构如图\ref{fig:server-architecture}所示。
|
||||||
|
|
||||||
|
\begin{figure}[htbp]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.8\textwidth]{assets/server-architecture.png}
|
||||||
|
\caption{服务端架构图}
|
||||||
|
\label{fig:server-architecture}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
在系统的架构中主要有如下几个部分组成:
|
||||||
|
\begin{itemize}
|
||||||
|
\item 模型层,是系统架构中的底层,也就是系统中的数据存取层。在这一层主要负责系统中需要和数据库进行交互的部分逻辑。
|
||||||
|
\item 服务层,是系统架构中的中层,是系统中业务逻辑主要存在的层。在这一层中负责完成系统需要完成的各种复杂逻辑,对下同模型层进行交互完成数据的增、删、查、改。对上同控制器层进行交互处理用户的请求。
|
||||||
|
\item 控制器层,是系统架构中的顶层,直接定义了同前端进行交互的逻辑。在这一层中声明了向前端提供各种接口的地址、请求方式和请求体,并负责对前端发送的请求进行校验和序列化为对象,在调度对应的服务得到结果之后序列化发送给前端。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\subsection{多媒体文件的扫描和元数据的获取}
|
||||||
|
|
||||||
|
在服务端中的核心逻辑之一便是如何快速而高效的从用户指定的媒体文件夹下扫描多媒体文件并从中提取出多媒体文件的元数据。本节中便重点阐述本段逻辑。
|
||||||
|
|
||||||
|
\begin{lstlisting}[style=csharp]
|
||||||
|
private async Task<IEnumerable<Song>> ScanSongAsync(DirectoryInfo directory, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ConcurrentBag<Song> songs = [];
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(directory.EnumerateDirectories(), cancellationToken, async (info, token) =>
|
||||||
|
{
|
||||||
|
foreach (Song song in await ScanSongAsync(info, token))
|
||||||
|
{
|
||||||
|
songs.Add(song);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(directory.EnumerateFiles(), cancellationToken, async (f, _) =>
|
||||||
|
{
|
||||||
|
if (!MediaItemTypes.MusicTypes.Contains(f.Extension))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TagLib.File tagFile = TagLib.File.Create(f.FullName);
|
||||||
|
|
||||||
|
IPicture? picture = tagFile.Tag.Pictures.FirstOrDefault();
|
||||||
|
string? coverImageUrl = null;
|
||||||
|
if (picture is not null)
|
||||||
|
{
|
||||||
|
coverImageUrl = "/api/file/" + await fileStore.UploadFileAsync(picture.Data.Data, picture.MimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
Song song = new()
|
||||||
|
{
|
||||||
|
Title = tagFile.Tag.Title ?? f.Name,
|
||||||
|
Arist = tagFile.Tag.FirstPerformer ?? "Default Arist",
|
||||||
|
Path = f.FullName,
|
||||||
|
Url = $"/api/file/{await fileStore.ClarifyLocalFileAsync(f.FullName, tagFile.MimeType)}",
|
||||||
|
CoverImageUrl = coverImageUrl ?? string.Empty,
|
||||||
|
Album = new Album
|
||||||
|
{
|
||||||
|
// 避免空应用错误
|
||||||
|
Title = tagFile.Tag.Album ?? "Default Album",
|
||||||
|
Arist = tagFile.Tag.FirstAlbumArtist ?? "Default Artist",
|
||||||
|
Path = f.Directory is null ? string.Empty : f.Directory.FullName,
|
||||||
|
ParentRepository = new MediaRepository()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
songs.Add(song);
|
||||||
|
}
|
||||||
|
catch (UnsupportedFormatException e)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Failed to parser file {}: {}.", f.Name, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return songs;
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
这段代码是用于扫描指定目录及其子目录下所有音乐文件,并从中提取元数据以创建Song对象的异步方法。这个方法不仅读取音乐文件的元数据(如标题、艺术家、专辑等),还尝试从文件中获取封面图片并上传到一个文件存储服务。
|
||||||
|
|
||||||
|
该递归方法接收一个DirectoryInfo对象和一个CancellationToken作为参数,用于取消正在进行的任务。使用ConcurrentBag<Song>来收集所有找到的歌曲信息。ConcurrentBag是线程安全的集合类型,适合多线程操作。Parallel.ForEachAsync用于异步并行地遍历目录下的所有子目录,递归调用ScanSongAsync方法。这样可以有效地利用多核处理器进行并行处理。
|
||||||
|
|
||||||
|
方法的核心是文件过滤和元数据提取。方法对目录下的所有文件进行遍历,仅处理扩展名属于预定义的MediaItemTypes.MusicTypes集合的文件。使用TagLib库读取音频文件的元数据,如标题、艺术家、专辑信息等,并尝试从音频文件中提取封面图片,并将其上传到文件存储服务,通过调用fileStore.UploadFileAsync方法。上传成功后,封面图片的URL被保存在Song对象中。如果遇到不支持的文件格式,会捕获UnsupportedFormatException异常,并记录相关信息。这有助于调试和避免因个别文件问题导致整个扫描过程失败。最终,该方法返回一个IEnumerable<Song>集合,包含了所有扫描到的歌曲信息。
|
||||||
|
|
||||||
|
\subsection{实时转码和推流实现}
|
||||||
|
|
||||||
|
服务端中的核心代码是实现视频的实时转码和推流,这是在服务端通过调用FFmpeg程序来实现的。
|
||||||
|
|
||||||
|
\begin{lstlisting}[style=csharp]
|
||||||
|
public record VideoConversion(Task<IConversionResult> RunningTask,
|
||||||
|
DirectoryInfo CacheDirectory,
|
||||||
|
string Name,
|
||||||
|
CancellationTokenSource CancellationTokenSource);
|
||||||
|
|
||||||
|
public class FfmpegService
|
||||||
|
{
|
||||||
|
public VideoConversion? CurrentConversion { get; set; }
|
||||||
|
|
||||||
|
public async Task<IConversionResult> StartConversion(FileInfo video, DirectoryInfo cacheDirectory, string name,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
IMediaInfo inputVideo = await FFmpeg.GetMediaInfo(video.FullName, cancellationToken);
|
||||||
|
|
||||||
|
IConversion conversion = FFmpeg.Conversions.New()
|
||||||
|
.AddStream(inputVideo.Streams)
|
||||||
|
.AddParameter("-re", ParameterPosition.PreInput)
|
||||||
|
.AddParameter("-c:v h264 -f hls")
|
||||||
|
.AddParameter("-profile:v high10")
|
||||||
|
.AddParameter("-hls_list_size 10 -hls_time 10 -hls_base_url /api/hls/")
|
||||||
|
.AddParameter("-hls_flags delete_segments")
|
||||||
|
.SetOutput(Path.Combine(cacheDirectory.FullName, $"{name}.m3u8"));
|
||||||
|
|
||||||
|
return await conversion.Start(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
这段C\#代码定义了一个FfmpegService类,用于处理视频转换任务,特别是将输入视频文件转换为HLS流格式,以便于在网络环境中播放。
|
||||||
|
|
||||||
|
VideoConversion是一个记录类型,它封装了一个正在进行的视频转换任务的状态。包括正在运行的任务(Task<IConversionResult>)、缓存目录(DirectoryInfo)、转换任务的名称(string)以及用于取消任务的CancellationTokenSource。这种设计允许外部代码追踪当前正在进行的转换任务状态,同时提供了一种优雅地取消转换任务的方法。
|
||||||
|
|
||||||
|
FfmpegService类中有一个CurrentConversion属性,它是VideoConversion?类型(可空的VideoConversion)。这意味着FfmpegService实例可以跟踪当前正在执行的视频转换任务。
|
||||||
|
|
||||||
|
StartConversion方法接受一个视频文件(FileInfo)、缓存目录(DirectoryInfo)、任务名称(string)和取消令牌(CancellationToken)作为参数。首先,它使用FFmpeg库的GetMediaInfo方法异步获取输入视频的媒体信息。然后,它创建一个新的转换流程,添加视频流信息、设置编码参数和输出格式。这里特别配置了HLS流的参数,例如-c:v h264表示使用H.264编码,-profile:v high10指定了编码的配置文件,-hls\_list\_size和-hls\_time控制了HLS播放列表的大小和片段持续时间。最后,它设置了输出文件路径(.m3u8格式的HLS播放列表)并启动转换任务,返回IConversionResult类型的异步任务。
|
||||||
|
|
||||||
|
该方法的设计提高了视频处理的效率,还提供了灵活的取消机制,这对于长时间运行的任务尤其重要。
|
||||||
|
|
||||||
|
|
||||||
|
\end{document}
|
36
AndroidReport/ch05-extensions.tex
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
\documentclass[main.tex]{subfiles}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
\section{系统可能的扩展}
|
||||||
|
|
||||||
|
%系统可能的进一步扩展应用
|
||||||
|
|
||||||
|
\subsection{界面美化和用户体验优化}
|
||||||
|
|
||||||
|
在未来的版本中,我们计划对界面进行深度美化和优化,以提供更加沉浸式的用户体验。这将包括:
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item 自定义主题:允许用户根据个人喜好选择不同的界面主题,如暗黑模式、多彩模式等,增强个性化体验。
|
||||||
|
\item 动态UI元素:引入动态背景、过渡效果以及动画,使界面更加生动有趣。
|
||||||
|
\item 响应式设计:确保应用在不同设备上(手机、平板、电视)都能呈现出最佳的视觉效果和操作体验。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\subsection{元数据刮削与智能推荐}
|
||||||
|
|
||||||
|
为了丰富媒体内容的信息,我们将引入元数据刮削功能,自动从互联网上抓取媒体文件的相关信息,如演员列表、导演信息、剧情简介等。同时,基于这些元数据,我们可以实现以下功能:
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item 智能推荐系统:通过分析用户的观看历史和偏好,结合元数据,为用户推荐相似或相关的内容,提高用户满意度。
|
||||||
|
\item 详细信息展示:在媒体播放页面下方显示详细的元数据信息,帮助用户了解更多的背景知识。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\subsection{播放记录与续播功能}
|
||||||
|
|
||||||
|
为了提升用户体验,我们将增加播放记录功能,具体包括:
|
||||||
|
|
||||||
|
\begin{itemize}
|
||||||
|
\item 自动保存播放位置:无论用户在哪个设备上观看,都可以无缝接续上次的播放进度,无需手动寻找。
|
||||||
|
\item 观看历史:记录用户的观看历史,方便用户回顾曾经观看过的媒体内容,同时可以作为推荐算法的数据来源之一。
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\end{document}
|
18
AndroidReport/ch06-summary.tex
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
\documentclass[main.tex]{subfiles}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
\section{总结体会}
|
||||||
|
|
||||||
|
%结合自己设计的系统,结合开发技术与开发平台的认识了解,对移动互联网技术的理解分析与体会
|
||||||
|
|
||||||
|
随着科技的迅猛发展和全球化的推动,互联网已经成为我们生活的一部分。在互联网中,移动互联网技术更是在短短数十年间,以其便捷、高效、个性化的特点,深深影响着我们的工作、学习和生活方式。在本次课程大作业开发中,我通过使用Jetpack Compose技术,成功开发了一款在线多媒体播放APP。在此过程中,我也对移动互联网技术有了更深的理解和感悟。
|
||||||
|
|
||||||
|
移动互联网技术是指通过移动设备如手机、平板电脑等进行网络访问的技术。它涵盖了移动通信技术、无线网络技术、移动终端技术以及各种移动应用和服务。而移动应用开发则是移动互联网技术的核心组成部分,其主要目标是创建能够满足用户需求、提供良好用户体验的应用程序。
|
||||||
|
|
||||||
|
在开发过程中,我使用了Jetpack Compose这一新的Android UI工具包,它采用声明式编程风格,简化了UI开发过程,提高了开发效率。Compose允许开发者用更少的代码创建更复杂的UI,同时提供了丰富的组件库和动画支持,使得多媒体应用的开发变得更加容易。
|
||||||
|
|
||||||
|
在开发过程中,我深深感受到移动互联网技术的更新速度之快。例如,Jetpack Compose自发布以来,不断迭代更新,新功能层出不穷,这要求开发者必须持续学习,跟上技术发展的步伐。:在设计应用时,我始终将用户体验放在首位。从界面设计到功能实现,都力求简洁明了,操作流畅,以提升用户的使用满意度。这也是移动互联网技术的核心理念——以用户为中心。在移动互联网时代,用户可能使用多种设备和操作系统。因此,跨平台开发成为一种趋势,需要开发者具备跨平台开发的能力,以确保应用在不同设备上的兼容性和一致性。:在移动应用开发中,数据安全和用户隐私保护是不可忽视的问题。在设计应用时,我注重对用户数据的加密存储和传输,遵守相关法律法规,尊重用户隐私。
|
||||||
|
|
||||||
|
总的来说,移动互联网技术的发展为我们带来了前所未有的机遇和挑战。作为一名计算机专业的学生,我将不断提升自己的专业技能,紧跟技术前沿,为用户提供更加优质、安全、个性化的移动应用服务。同时,我也认识到,技术的发展应服务于人类社会的进步,我们应当用技术的力量,创造一个更加美好的世界。
|
||||||
|
|
||||||
|
\end{document}
|
135
AndroidReport/main.tex
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
\documentclass[12pt, a4paper, oneside]{ctexart}
|
||||||
|
\usepackage{amsmath, amsthm, amssymb, appendix, bm, graphicx, hyperref, mathrsfs, geometry}
|
||||||
|
\usepackage{float}
|
||||||
|
\usepackage{subcaption}
|
||||||
|
\usepackage{listings}
|
||||||
|
\usepackage{longtable}
|
||||||
|
\usepackage{tabularx}
|
||||||
|
\usepackage[dvipsnames]{xcolor}
|
||||||
|
\usepackage{subfiles}
|
||||||
|
\usepackage{fontspec}
|
||||||
|
\usepackage{array}
|
||||||
|
\usepackage{multirow}
|
||||||
|
\usepackage{booktabs}
|
||||||
|
|
||||||
|
\linespread{1.5}
|
||||||
|
\pagestyle{plain}
|
||||||
|
\geometry{a4paper, scale=0.8}
|
||||||
|
|
||||||
|
% 定义书写Kotlin时的listings style
|
||||||
|
\lstdefinelanguage{Kotlin}{
|
||||||
|
comment=[l]{//},
|
||||||
|
commentstyle={\color{gray}\ttfamily},
|
||||||
|
emph={filter, first, firstOrNull, forEach, lazy, map, mapNotNull, println},
|
||||||
|
emphstyle={\color{OrangeRed}},
|
||||||
|
identifierstyle=\color{black},
|
||||||
|
keywords={!in, !is, abstract, actual, annotation, as, as?, break, by, catch, class, companion, const, constructor, continue, crossinline, data, delegate, do, dynamic, else, enum, expect, external, false, field, file, final, finally, for, fun, get, if, import, in, infix, init, inline, inner, interface, internal, is, lateinit, noinline, null, object, open, operator, out, override, package, param, private, property, protected, public, receiveris, reified, return, return@, sealed, set, setparam, super, suspend, tailrec, this, throw, true, try, typealias, typeof, val, var, vararg, when, where, while},
|
||||||
|
keywordstyle={\color{NavyBlue}\bfseries},
|
||||||
|
morecomment=[s]{/*}{*/},
|
||||||
|
morestring=[b]",
|
||||||
|
morestring=[s]{"""*}{*"""},
|
||||||
|
ndkeywords={@Deprecated, @JvmField, @JvmName, @JvmOverloads, @JvmStatic, @JvmSynthetic, Array, Byte, Double, Float, Int, Integer, Iterable, Long, Runnable, Short, String, Any, Unit, Nothing},
|
||||||
|
ndkeywordstyle={\color{BurntOrange}\bfseries},
|
||||||
|
sensitive=true,
|
||||||
|
breaklines=true,
|
||||||
|
showstringspaces=false,
|
||||||
|
stringstyle={\color{ForestGreen}\ttfamily},
|
||||||
|
}
|
||||||
|
|
||||||
|
% 定义书写C#时的listings style
|
||||||
|
\lstdefinestyle{csharp}{
|
||||||
|
language=[sharp]c,
|
||||||
|
breaklines=true,
|
||||||
|
basicstyle=\ttfamily,
|
||||||
|
keywordstyle=\bfseries\color{violet},
|
||||||
|
emphstyle=\bfseries\color{blue},
|
||||||
|
morekeywords={required, get, set, init, async, await},
|
||||||
|
showstringspaces=false,
|
||||||
|
}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
\begin{titlepage}
|
||||||
|
% 标题
|
||||||
|
\begin{center}
|
||||||
|
\Huge{\textbf{移动互联网技术及应用}}
|
||||||
|
|
||||||
|
\vspace{2em}
|
||||||
|
|
||||||
|
\Huge{\textbf{大作业报告}}
|
||||||
|
|
||||||
|
\vspace{5em}
|
||||||
|
|
||||||
|
\Large{\textbf{题目:} \underline{在线多媒体播放系统的设计与实现}}
|
||||||
|
|
||||||
|
\vspace{3em}
|
||||||
|
|
||||||
|
\large{\textbf{类型:} \underline{应用系统设计实现}}
|
||||||
|
|
||||||
|
\end{center}
|
||||||
|
|
||||||
|
\vspace{6em}
|
||||||
|
|
||||||
|
% 个人信息
|
||||||
|
\begin{center}
|
||||||
|
\large{\makebox[4em][c]{姓名:} \underline{\makebox[8em][c]{任昌骏}}}
|
||||||
|
|
||||||
|
\large{\makebox[4em][c]{班级:} \underline{\makebox[8em][c]{2021211308}}}
|
||||||
|
|
||||||
|
\large{\makebox[4em][c]{学号:} \underline{\makebox[8em][c]{2021211180}}}
|
||||||
|
\end{center}
|
||||||
|
|
||||||
|
% 封底
|
||||||
|
\vspace{8em}
|
||||||
|
|
||||||
|
\begin{center}
|
||||||
|
\Large{2024年6月}
|
||||||
|
\end{center}
|
||||||
|
\end{titlepage}
|
||||||
|
|
||||||
|
\clearpage
|
||||||
|
|
||||||
|
% 目录
|
||||||
|
% 目录的页码和正文的页码不一致
|
||||||
|
\pagenumbering{Roman}
|
||||||
|
\setcounter{page}{1}
|
||||||
|
\tableofcontents
|
||||||
|
|
||||||
|
\clearpage
|
||||||
|
\setcounter{page}{1}
|
||||||
|
\pagenumbering{arabic}
|
||||||
|
|
||||||
|
\section{引言}
|
||||||
|
|
||||||
|
随着科技的发展,在移动终端上查看存储在家庭内部服务器内的多媒体资源成为一个越来越常见的需求。在本次的课程作业中,我们便设计了一个基于Androin终端的在线多媒体播放器,系统名称\textbf{Chiara}。在本篇报告中,我们将综述在系统中涉及的相关技术、分析该系统的功能需求,介绍该系统的设计与实现细节,并进一步说明项目后续可能的扩展方向。在报告的最后,我们将介绍在设计和实现该系统过程中的总结和体会。
|
||||||
|
|
||||||
|
\clearpage
|
||||||
|
|
||||||
|
\subfile{ch01-technology}
|
||||||
|
|
||||||
|
\clearpage
|
||||||
|
|
||||||
|
\subfile{ch02-requirements}
|
||||||
|
|
||||||
|
\clearpage
|
||||||
|
|
||||||
|
\subfile{ch03-design}
|
||||||
|
|
||||||
|
\clearpage
|
||||||
|
|
||||||
|
\subfile{ch04-server}
|
||||||
|
|
||||||
|
\clearpage
|
||||||
|
|
||||||
|
\subfile{ch05-extensions}
|
||||||
|
|
||||||
|
\clearpage
|
||||||
|
|
||||||
|
\subfile{ch06-summary}
|
||||||
|
|
||||||
|
\clearpage
|
||||||
|
|
||||||
|
\bibliographystyle{unsrt}
|
||||||
|
\bibliography{ref}
|
||||||
|
|
||||||
|
\end{document}
|
13
AndroidReport/ref.bib
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
@misc{ kotlin_docs,
|
||||||
|
author = {Jetbrains},
|
||||||
|
title = {{Kotlin Docs}},
|
||||||
|
howpublished = {Website},
|
||||||
|
url = {https://kotlinlang.org/docs/home.html},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{ compose_docs,
|
||||||
|
author = {Google Android},
|
||||||
|
title = {{Jetpack Compose Docs}},
|
||||||
|
howpublished = {Website},
|
||||||
|
url = {https://developer.android.google.cn/develop/ui/compose/documentation?hl=zh-cn},
|
||||||
|
}
|