feat: add Android Report

This commit is contained in:
jackfiled 2024-07-04 21:45:47 +08:00
parent a90302f7ba
commit 2414d1b66b
25 changed files with 1564 additions and 0 deletions

7
AndroidReport/.latexmkrc Normal file
View 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";

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

View 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 InjectionDI和控制反转Inversion of ControlIoC是软件工程中特别是在面向对象编程中用于降低代码耦合度和提高模块间解耦的重要设计模式和原则。
控制反转是一种设计原则它描述了软件组件控制流的反转。在传统的编程模式下组件控制着它们的依赖项的创建和生命周期而在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 清单文件PlaylistHLS使用一个M3U8格式的文本文件作为清单其中包含了指向媒体片段的URL链接以及元数据。客户端通过解析这个清单文件来获取并播放媒体片段。
\item 广泛的支持HLS不仅被苹果的设备和平台所支持也被许多其他厂商和流媒体播放器所采纳包括Android设备、智能电视、游戏机和网页浏览器等。
\end{itemize}
HLS的工作流程通常如下服务器将视频流切分为多个小的TSTransport 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生成HLSHTTP 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}

View 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}

View 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应该也是一个枚举类型定义了播放器的重复模式例如SequenceRepeatOneRepeatAll等
通过这个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}

View 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}

View 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}

View 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
View 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
View 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},
}