65.9K
CodeProject 正在变化。 阅读更多。
Home

Gallery Server Pro - 一个用于共享照片、视频、音频和其他媒体的 ASP.NET 相册

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (123投票s)

2007 年 10 月 28 日

GPL3

31分钟阅读

viewsIcon

872788

downloadIcon

2622

Gallery Server Pro 是一个完整、稳定的 ASP.NET 相册,用于共享照片、视频、音频和其他媒体。本文介绍了整体架构和主要功能。

Screen shot of Gallery Server Pro

目录

引言

背景

运行 Gallery Server Pro

解决方案架构

用户管理和安全

Entity Framework Code First 迁移

基于模板的用户界面

渲染视频、图像、音频等的 HTML

使用 FFmpeg 转码视频和音频

媒体对象、相册和组合模式

使用策略模式持久化到数据存储

摘要

引言

Gallery Server Pro 是一个功能强大且易于使用的数字资产管理 (DAM) 应用程序和 Web 相册,用于共享和管理照片、视频、音频和其他文件。它是根据 GPL v3 发布并使用 ASP.NET 和 C# 编写的开源软件。整个应用程序包含在一个 ASCX 用户控件中,可以轻松插入到您自己的网站中。

  • 将媒体文件组织到相册中,您可以轻松添加、编辑、删除、旋转、排序、复制和移动它们
  • 基于 jsRender 模板的用户界面,您可以轻松修改
  • 使用 HTML5 和 CSS3 实现即插即用的视频和音频,必要时回退到 Flash 和 Silverlight
  • 通过一键同步和 ZIP 文件上传功能添加文件。会自动创建缩略图和压缩版本
  • 支持 YouTube 视频等外部媒体对象
  • 可扩展至数十万个对象
  • 强大的用户安全功能,具有灵活的、按相册粒度的控制
  • 元数据提取
  • 图像水印
  • SQL CE 或 SQL Server 数据库
  • 100% 托管代码,使用 C# 和 ASP.NET 4.5 编写
  • 源代码根据开源的 GNU General Public License v3 发布

您可以尝试 Gallery Server Pro 的在线演示,以了解其功能。预编译版本已提供,包括其他文档和支持论坛。

背景

该项目始于 2002 年,源于我希望在网上分享照片的愿望。我想让我的照片保留在自己的服务器上,而不是像 Flicker 或 Facebook 那样的第三方服务器。由于当时没有免费解决方案可供选择,我便自己写了一个。

第 1 版于 2006 年 1 月发布。自那时以来,已有数十万次下载和持续不断的版本发布。在撰写本文时,最新版本是 3.0.3。请访问 galleryserverpro.com 获取最新版本。

在本文中,我将介绍 Gallery Server Pro 的整体架构和技术特性。这里介绍的主题可能对您想了解更多关于以下方面的内容有帮助:

  • 实现用于共享照片、视频、音频和其他文档的 Web 相册
  • 使用 Entity Framework Code First 迁移
  • 使用 jsRender 渲染 HTML
  • 创建一个允许用户从站点管理员区域修改 jsRender 模板的应用程序
  • 使用 FFmpeg 从 ASP.NET 转码视频和音频
  • 使用组合设计模式管理无限层级的关系。
  • 何时以及如何使用策略设计模式

运行 Gallery Server Pro

Gallery Server Pro 是一个功能齐全且稳定的 Web 应用程序,可供生产使用。要使用编译版本,请访问下载页面并按照管理员指南中的安装说明进行操作。

既然您正在阅读本文,您可能更倾向于从源代码开始。您需要使用 Visual Studio 2012 或更高版本。方法如下:

  1. 下载源代码。解压缩文件之前请先取消阻止 ZIP 文件。(在 Windows 资源管理器中右键单击文件,选择“属性”,然后在“常规”选项卡上单击“取消阻止”。如果看不到此按钮,则忽略此消息。)
  2. 将目录和文件提取到所需位置。
  3. 启动 Visual Studio 并打开解决方案文件。
  4. 默认情况下,代码配置为使用 SQL CE 数据库。如果您想使用 SQL Server,请打开 web.config,取消注释 SQL Server 连接字符串并编辑它以提供凭据。然后注释掉或删除 SQL CE 连接字符串。
  5. 就是这样!按 F5 运行网站。相册应自动在浏览器中显示。单击链接以创建管理员帐户。此时,您可以像正常安装一样使用相册。

Gallery Server Pro 将媒体对象存储在分层的相册中。媒体对象是任何类型的文件或 HTML 代码块,例如我们在许多网站上找到的嵌入代码。媒体文件和相册存储在 Web 应用程序内的名为 *gs\mediaobjects* 的目录中。(这可以更改为任何可网络访问的位置。)相册实际上就是一个目录,因此名为“Vacation Photos”的相册存储为一个同名目录。

添加媒体对象主要有两种技术:

  1. 上传包含媒体文件的 ZIP 文件。如果 ZIP 文件包含目录,则会将它们转换为相册。
  2. 将您的媒体文件复制到媒体对象目录,然后启动 Gallery Server Pro 中的同步。

添加媒体对象时,会执行以下步骤:

  1. 文件被保存到媒体对象目录。(如果是通过同步技术添加媒体对象,则此步骤已完成。)
  2. 提取文件的元数据(例如,相机型号、快门速度、视频时长等)。
  3. 创建一个缩略图图像并保存到硬盘。
  4. 为图像、视频和音频文件创建一个压缩的、节省带宽的版本。
  5. 将一条记录添加到数据存储中以表示此媒体对象。

媒体对象通过 HTTP 处理程序流式传输到浏览器。下面您可以看到正在显示的图片和视频。如果启用了水印,则在图像发送到浏览器之前,水印会被应用于图像的内存版本。

Screenshot - imageview.jpg Screenshot - videoview.png

右侧窗格显示媒体对象的元数据。这些项的布局和可编辑性是高度可配置的。

Screenshot - metadata.png

默认情况下,所有人都可以浏览媒体对象。但是,您必须登录才能执行任何修改相册或媒体对象的操作。修改数据的授权通过权限类型及其适用的相册进行配置。例如,您可以为用户 **Soren** 设置对相册 **Soren's photos** 的编辑权限。另一个用户 **Margaret** 被授予对相册 **Margaret's photos** 的编辑权限。每个用户都可以管理自己的相册,但不能编辑自己领域之外的相册。

要从最终用户角度了解更多有关如何使用 Gallery Server Pro 的信息,请阅读管理员指南。否则,请继续阅读以了解架构和编程技术。

解决方案架构

源代码包含六个项目。它们是:

项目名称 描述
网站 UI 层 - ASP.NET 4.5 Web 应用程序
TIS.GSP.Business 业务层逻辑
TIS.GSP.Business.Resources 包含支持业务层的资源数据
TIS.GSP.Business.Interfaces 定义解决方案中使用的所有接口
TIS.GSP.Events 提供事件处理支持
TIS.GSP.Data.Data 数据层逻辑

用户管理和安全

用户帐户通过 ASP.NET Membership 和 Roles API 进行管理。默认情况下,Gallery Server Pro 配置为使用位于 App_Data 目录中的本地存储的 SQL CE 数据库,名为 *GalleryDb.sdf*。它通过使用 System.Web.Providers.DefaultMembershipProvider 用于用户,以及 System.Web.Providers.DefaultRoleProvider 用于角色来与此数据库进行交互。

由于提供程序模型提供的灵活性,您可以使用任何具有成员资格提供程序的(数据)存储。例如,您可以使用 SqlMembershipProvider 来使用 SQL Server,或使用 ActiveDirectoryMembershipProvider 将 Gallery Server Pro 集成到您现有的 Active Directory 用户群中。管理员指南包含一个关于成员资格配置的章节,提供了更多信息。

Entity Framework Code First 迁移

概述

Gallery Server Pro 3.0 之前的版本使用数据提供程序模型与数据库进行交互。这需要创建继承自 System.Configuration.Provider.ProviderBase 的类,为每个支持的数据技术提供特定的实现。这种方法有一些优点,例如能够针对多个数据库,并允许每个数据实现利用平台的优势。例如,SQL Server 数据提供程序在所有数据访问中使用存储过程。

但是,这种方法有一个严重的缺点——所有数据访问代码必须为每个数据提供程序编写一次。由于 Gallery Server Pro 附带了两个提供程序——SQL CE 和 SQL Server——这意味着重复了每段数据访问代码。当然,测试也翻倍了。为了进一步复杂化,更新每个版本数据架构的升级脚本也必须重复。这种代码重复违反了DRY 原则,减慢了开发速度,并且坦率地说,开发和维护起来并不有趣。

因此,我们在 3.0 版本中切换到了 Entity Framework Code First Migrations。主要好处是我们编写一段 LINQ 代码来与数据库交互。EF 框架负责为 SQL CE 或 SQL Server 生成正确的 SQL 语法。例如,要从数据库检索一个相册,我们只需要编写这个:

using (var repo = new AlbumRepository())
{
  var album = repo.Where(a => a.AlbumId == albumId);
}

EF 为我们处理其余部分,这意味着我们不必处理 ADO.NET 命令、连接或数据库特定的细节。甚至更好的是,迁移支持意味着我们可以用数据库无关的代码添加一个列到表中,如下所示:

AddColumn("gsp.Metadata", "RawValue", c => c.String(maxLength: 1000)); 

当相册 Web 应用程序启动时,EF 初始化代码会检查数据库的当前版本,并在必要时自动升级它。

数据库要求

EF Code First Migrations 必须满足以下要求:

  • 在相册首次启动或任何时候丢失时,自动创建数据库、对象和初始数据。
  • 如果数据库存在但还没有任何相册对象,则创建对象并用初始数据填充它。
  • 如果存在早期版本的相册数据架构,则自动将其升级到当前版本。它必须支持从 3.0.0 起的所有 Gallery Server Pro 以前版本的升级。

这些要求必须同时满足,而无需更改 web.config 或任何其他用户输入。事实证明,这是一个真正的挑战,但最终我们还是解决了。

创建和更新数据库

为了让 EF 自动管理数据库,包括按需创建、填充和更新,我们在启动时运行以下代码:

private static void InitializeDataStore()
{
  System.Data.Entity.Database.SetInitializer(new System.Data.Entity.MigrateDatabaseToLatestVersion<GalleryDb, GalleryDbMigrationConfiguration>());

  var configuration = new GalleryDbMigrationConfiguration();
  var migrator = new System.Data.Entity.Migrations.DbMigrator(configuration);
  if (migrator.GetPendingMigrations().Any())
  {
    migrator.Update();
  }
}

此代码注册 MigrateDatabaseToLatestVersion 数据库初始化程序,并传入 GalleryDbMigrationConfiguration 类的引用。然后,它检查是否有待处理的迁移,如果有,则执行它们。GalleryDbMigrationConfiguration 类如下所示:

public sealed class GalleryDbMigrationConfiguration : DbMigrationsConfiguration<GalleryDb>
{
  protected override void Seed(GalleryDb ctx)
  {
    MigrateController.ApplyDbUpdates();
  }
}

在所有迁移完成后,会调用 Seed 方法,因此我们可以确保数据库架构与 Code First 数据模型匹配(假设我们正确编写了迁移的 Up() 方法)。此方法是插入种子数据或修改支持新版本所需数据的良好位置。例如,AppSetting 表包含一个记录,存储当前数据架构版本(例如,“3.0.3”)。当相册更新到新版本时,也必须更新此记录。

起初,您可能会认为在迁移的 Up() 方法中更新记录是一个好地方,事实上,Gallery Server Pro 的早期版本确实这样做了。然而,当架构更新时,这种方法可能会失败。例如,假设我们在 AppSetting 表中添加了一个新列。迁移的 Up() 方法将包含一个调用 AddColumn 来添加新列的调用,但更改实际上在退出 Up() 方法后的某个时间才会传播到表中。这意味着从 Up() 方法尝试查询 AppSetting 表将失败,因为实体模型(例如 Code First AppSetting 类)包含新列,但数据库表不包含。

这就是为什么 Seed 方法是插入、更新或删除数据库记录的好地方。我们编写了一个 MigrateController 类来处理这些数据更新:

public static void ApplyDbUpdates()
{
  using (var ctx = new GalleryDb())
  {
    if (!ctx.AppSettings.Any())
    {
      SeedController.InsertSeedData(ctx);
    }

    var curSchema = GetCurrentSchema(ctx);

    while (curSchema < GalleryDb.DataSchemaVersion)
    {
      var oldSchema = curSchema;

      switch (curSchema)
      {
        case GalleryDataSchemaVersion.V3_0_0: UpgradeTo301(); break;
        case GalleryDataSchemaVersion.V3_0_1: UpgradeTo302(); break;
        case GalleryDataSchemaVersion.V3_0_2: UpgradeTo303(); break;
        case GalleryDataSchemaVersion.V3_0_3: UpgradeTo310(); break;
      }

      curSchema = GetCurrentSchema(ctx);

      if (curSchema == oldSchema)
      {
        throw new Exception(String.Format("The migration function for schema {0} should have incremented the data schema version in the AppSetting table, but it did not.", curSchema));
      }
    }
  }
}

private static GalleryDataSchemaVersion GetCurrentSchema(GalleryDb ctx)
{
  return GalleryDataSchemaVersionEnumHelper.ConvertGalleryDataSchemaVersionToEnum(ctx.AppSettings.First(a => a.SettingName == "DataSchemaVersion").SettingValue);
}

该方法首先在 AppSetting 表中找不到记录时插入种子数据。这将在首次安装相册时发生。InsertSeedData() 方法使用查找数据和默认设置填充多个表。如果您好奇,请参阅源代码。

接下来,该函数从数据库的 AppSetting 表中获取当前数据架构版本,并使用它来调用相应的更新方法。请参阅源代码以了解这些函数,但可以肯定地说,它们更新了 MediaTemplate 和 UiTemplate 等各种表,以修复错误并更新行为。它们还将 AppData 表中的数据架构版本递增。

我们不会详细介绍迁移的创建过程,因为这很常见,并且可以在网上轻松找到 Code First Migrations 的相关信息。简而言之,我们在 Visual Studio 的包管理器控制台中执行命令来启用迁移 (Enable-Migrations) 并添加它们(例如,Add-Migration v3.1.0)。无需调用 Update-Database,因为 MigrateDatabaseToLatestVersion 数据库初始化程序会在我们第一次运行应用程序时为我们处理。

指定数据库

您可能已经注意到上面的节点没有明确引用 SQL Server 或 SQL CE。相反,它是数据库无关的,并且可以很好地同时处理两者。那么,我们在哪里指定要使用的数据库技术以及其他相关信息,如身份验证信息?这一切都通过 Web 应用程序根目录中的 web.config 文件中的连接字符串来处理:

<connectionStrings>
  <clear />
  <add name="GalleryDb" providerName="System.Data.SqlClient" connectionString="server=(local);uid=;pwd=;Trusted_Connection=yes;database=GalleryServerPro;Application Name=Gallery Server Pro;MultipleActiveResultSets=True" />
</connectionStrings>

我们应用程序中的 DbContext 名为 GalleryDb,因此 EF 会查找名为该名称的连接字符串。在上例中,连接字符串指定了提供程序 System.Data.SqlClient (SQL Server),并告诉 EF 服务器位置、如何进行身份验证以及数据库名称。应用程序启动时,EF 会查找数据库,如果需要则创建它。然后它创建数据库对象并用数据填充。当第一个页面加载时,您将拥有一个完全配置的数据库,并且您的相册已准备就绪。

如果想使用 SQL CE,唯一需要的更改是更新连接字符串:

<connectionStrings>
  <clear />
  <add name="GalleryDb" providerName="System.Data.SqlServerCe.4.0" connectionString="data source=|DataDirectory|\GalleryData.sdf" />
</connectionStrings>

下次应用程序启动时,EF 会检测到更新的连接字符串,创建 SQL CE 数据库,用数据填充它,并开始使用它。

这种行为在开发过程中非常有用,因为它意味着您可以随时删除数据库,并在下次页面加载时自动创建它。由于这与最终用户安装相册时发生的过程相同,因此它是一种测试安装过程的便捷方法,以验证默认设置或其他设置行为是否按预期发生。

注意:如果您在 App_Data 目录中放置一个名为 install.txt 的空文本文件,则启动行为会略有变化。主页上会出现一个链接,让您可以创建一个管理员帐户。由于您需要管理员帐户来管理相册,因此这通常是您在 EF 创建数据库时想要执行的操作。

基于模板的用户界面

概述

UI 模板是一项强大的功能,它允许您仅使用 HTML 和 JavaScript 技能来修改用户界面。例如,您可以使用 UI 模板执行以下操作:

  • 在页眉中添加您的公司徽标
  • 鼠标悬停在缩略图上时添加图像预览
  • 在相册树中添加指向常用标签的链接
  • 在相册顶部显示最近添加的五个项目

我们将在本节后面介绍如何完成所有这些示例以及更多示例。但首先,让我们掌握基础知识。相册中有五个部分是使用 UI 模板构建的:

这些图像显示了五个模板:页眉、左窗格、右窗格、相册和媒体对象。中间窗格显示相册或媒体对象模板,具体取决于用户查看的是相册还是单个媒体对象。

什么不是用 UI 模板构建的?

主浏览界面是用模板构建的,但重要的是要了解哪些部分不是:

操作菜单和相册面包屑 - 操作菜单和相册面包屑链接不属于任何模板。相反,它的 HTML 是在服务器上生成的,并插入到页眉下的页面中。Gallery Server Pro 的未来版本可能会将此部分合并到页眉模板中,以便整个页面(如上面屏幕截图所示)完全由模板驱动。

任务页面 - 这些是执行任务的页面,例如上传文件、编辑说明等。这些页面是 ASCX 用户控件,不基于 UI 模板。

站点管理区域 - 站点管理区域中的任何页面都不是用 UI 模板构建的。与任务页面一样,它们是 ASCX 用户控件,可以通过编辑源代码来修改。

上面显示的每个部分都是从 UI 模板渲染的。UI 模板由包含 jsRender 语法的 HTML 片段和一些 JavaScript 组成,这些 JavaScript 告诉浏览器如何处理 HTML。通常,脚本会调用 jsRender 模板引擎并将生成的 HTML 追加到页面。客户端提供了大量相册数据,让您可以访问重要信息,如相册和媒体对象数据、用户权限和相册设置。我们将在本文后面介绍此数据的结构。

您可以在站点管理区域的 UI 模板页面上查看 UI 模板定义。在这两张图片中,您可以看到 HTML 和 JavaScript 值:

您可以在“目标相册”选项卡上分配此特定模板适用于哪些相册。如果多个模板以同一个相册为目标,则最具体的模板获胜。例如,如果默认模板分配给“所有相册”,而第二个模板分配给“样本”相册,则第二个模板将用于“样本”相册及其任何子项,因为该模板定义“更接近”相册。

预览选项卡允许您在保存更改之前查看编辑结果。

左窗格结构

让我们仔细看看其中一个模板是如何工作的。我们先选择左窗格模板。HTML 很简单:

<div id='{{:Settings.ClientId}}_lptv'></<div>

它定义了一个单独的空 div 标签并为其分配了一个唯一的 ID。双括号之间的文本是 jsRender 语法,引用了 Settings 对象的 ClientId 属性。此特定属性提供了一个对页面上 Gallery 用户控件当前实例唯一的字符串。当它在页面上渲染时,您将得到类似以下的 HTML:

<div id='gsp_g_lptv'></<div>

注意:对于大多数相册,定义唯一 ID 不是必需的,但它适用于希望在页面上包含两个相册控件实例的管理员。例如,您可能在一个网页部分有一个幻灯片放映,在另一个部分有一个视频播放。

敏锐的观察者会注意到,单个 div 标签看起来一点也不像复杂的树状视图。那么,那个 div 标签最终是如何变成树状视图的呢?让我们看看模板中的 JavaScript:

// Render the left pane, but not for touchscreens UNLESS the left pane is the only visible pane
var isTouch = window.Gsp.isTouchScreen();
var renderLeftPane = !isTouch  || (isTouch && ($('.gsp_tb_s_CenterPane:visible, .gsp_tb_s_RightPane:visible').length == 0));

if (renderLeftPane ) {
 $('#{{:Settings.LeftPaneClientId}}').html( $.render [ '{{:Settings.LeftPaneTmplName}}' ]( window.{{:Settings.ClientId}}.gspData ));

 var options = {
  albumIdsToSelect: [{{:Album.Id}}],
  navigateUrl: '{{:App.CurrentPageUrl}}'
 };

 // Call the gspTreeView plug-in, which adds an album treeview
 $('#{{:Settings.ClientId}}_lptv').gspTreeView(window.{{:Settings.ClientId}}.gspAlbumTreeData, options);
}

严格来说,这段文本不是纯 JavaScript。看到了其中的 jsRender 语法吗?没错,它也是一个 jsRender 模板,在执行前会通过 jsRender 引擎运行以生成纯 JavaScript。这是一个极其强大的功能,可以用来生成各种 UI 可能性。想象一下编写一个脚本,该脚本循环遍历相册中的图像以计算某个值,或调用服务器回调以请求有关特定相册的数据。

脚本首先决定左窗格模板是否应该附加到页面。我们决定不在触摸屏上显示它,原因有两个:(1) 触摸屏通常屏幕较小,无法承担左窗格所需的空间(2)分隔左、中、右窗格的分割器控件在触摸屏上效果不佳。

脚本引用了 window.Gsp.isTouchScreen() 函数。您可以在 gs\script\gallery.min.js 文件中找到此函数。如果您正在编辑相册,您可能更喜欢加载未压缩的脚本文件到浏览器中。您可以通过在 web.config 中将 debug 设置更改为“true”来轻松实现这一点。

如果脚本决定渲染左窗格,它会执行此行:

$('#{{:Settings.LeftPaneClientId}}').html( $.render [ '{{:Settings.LeftPaneTmplName}}' ]( window.{{:Settings.ClientId}}.gspData ));

任何熟悉 jsRender 或其他类型模板引擎的人都会对这感到熟悉。通俗地说,它意味着取名为 LeftPaneTmplName 的模板和 gspData 变量中的数据,通过 jsRender 引擎运行它,并将生成的 HTML 分配给名为 LeftPaneClientId 的 HTML 元素。换句话说,它将字符串 <div id='{{:Settings.ClientId}}_lptv'></div> 转换为 <div id='gsp_g_lptv'></div>,并将其添加到页面的 HTML DOM 中。

到目前为止,脚本所做的只是将一个 div 标签添加到页面,但我们仍然需要将其转换为相册树。这就是最后一个部分的作用:

var options = {
 albumIdsToSelect: [{{:Album.Id}}],
 navigateUrl: '{{:App.CurrentPageUrl}}'
};

// Call the gspTreeView plug-in, which adds an album treeview
$('#{{:Settings.ClientId}}_lptv').gspTreeView(window.{{:Settings.ClientId}}.gspAlbumTreeData, options);

上面提到的 JavaScript 文件包含多个 jQuery 插件,有助于降低模板的复杂性,并最大化浏览器缓存的脚本量(内联脚本永远不会被缓存)。这里我们使用 jQuery 来获取我们生成的 div 标签的引用,并在其上调用 gspTreeView 插件,传递相册树状视图数据和一些选项。树数据是一个 JSON 对象,包含相册结构,并在每次页面请求时都包含。反过来,gspTreeView 插件是第三方 jQuery 树控件 jsTree 的包装器,其代码位于 lib.min.js 文件中(如果您以 debug=true 运行,则为 lib.js)。该文件中包含几个第三方脚本库。

树状视图插件根据数据构建 HTML 树,并将其附加到 div 标签,从而生成您在左窗格中看到的树状视图。您可以尝试 HTML 和 JavaScript,然后使用预览选项卡查看这些编辑如何影响输出。

客户端数据模型

每个页面都包含一个丰富的数据集,该数据集随浏览器请求一起提供。存在一个名为 gspData 的全局 JavaScript 变量,它作用于当前 Gallery 用户控件实例的范围。在默认安装中,您可以在 window.gsp_g.gspData 中找到它,如 Chrome 的此图像所示:

让我们简要看一下每个顶级属性:

ActiveGalleryItems - 当前选定的 GalleryItem 实例数组。GalleryItem 可以代表一个相册或媒体对象(照片、视频、音频、文档、YouTube 片段等)。

ActiveMetaItems - 描述当前选定项的 MetaItem 实例数组。

Album - 关于当前相册的信息。它有两个重要的属性值得解释:GalleryItems 和 MediaItems。两者都代表相册和媒体对象,但 GalleryItem 实例仅包含每个项目的基本信息,而 MediaItem 实例包含项目的所有元数据和其他详细信息。由于 GalleryItem 实例很轻便,因此非常适合仅需要基本信息的相册缩略图视图。因此,为了优化传递给浏览器的数据,当查看相册时 MediaItems 属性为 null,当查看单个媒体对象时 GalleryItems 属性为 null。

App - 关于应用程序范围设置的信息,例如皮肤路径和当前 URL。

Resource - 包含语言资源。

Settings - 包含相册特定的设置。

User - 关于当前用户信息。

以下是客户端对象及其属性的完整列表(单击图像可放大):

有关客户端 API 的更多文档可以在管理员指南(查找 UI 模板部分)和GalleryServerPro.Web.Entity API 文档中找到。

渲染视频、图像、音频等的 HTML

概述

在浏览器中渲染 JPG 图像很简单,因为所有浏览器都使用基于 <img> HTML 标签的相同语法。但是视频、音频和其他文件类型的浏览器支持级别以及同一浏览器不同版本之间的支持级别差异很大。例如,某些浏览器可以原生播放 H.264 MP4 视频,而其他浏览器则需要 Flash 或 Silverlight 等插件。

Gallery Server Pro 通过一组媒体模板解决了这个问题,这些模板可以为单个浏览器甚至浏览器版本提供正确的 HTML 和 JavaScript。包含一组默认模板,对于大多数用户来说效果很好,但某些组织可能需要不同的行为。或者,当发布新浏览器版本时,其对特定媒体类型的支持可能会发生变化,从而需要更改使用的 HTML 语法。

媒体模板页面允许用户管理不同媒体类型的渲染行为。

上面的屏幕截图显示了用于“默认”浏览器在渲染 MIME 类型为 video/mp4 的媒体文件时使用的媒体模板。请注意,它指定了 HTML5 <video> 标签。当 MP4 视频在大多数浏览器中查看时,HTML 是基于此模板的,如下面的 Safari 浏览器所示:

浏览器 ID 下拉菜单允许用户查看其他浏览器的媒体模板。例如,MP4 模板包含两个额外的模板来支持 IE 1-8 和 Opera,它们不支持 HTML5 video 标签:

请注意,IE 1-8 和 Opera 的 HTML 模板是相同的,并且包含一个超链接标签。这定义了使用 FlowPlayer 的 HTML,FlowPlayer 使用 Flash 插件来渲染视频。FlowPlayer 需要一些 JavaScript 来初始化它,您可以在单击 JavaScript 选项卡时看到这一点:

当 MP4 视频在 IE 1-8 或 Opera 中查看时,它将获得基于相应模板的 HTML 和 JavaScript,如下面 Opera 中的屏幕截图所示:

通过使用针对各个浏览器的自定义媒体模板,Gallery Server Pro 可以提供有针对性的行为,而无需复杂的 HTML 来包含回退代码和其他技巧。管理员指南中有更多关于编辑和创建媒体模板的信息。

使用 FFmpeg 转码视频和音频

概述

FFmpeg 是一个开源实用程序,可以转码视频和音频,并从视频中提取缩略图。当 ffmpeg.exe 存在于 bin 目录中时,Gallery Server Pro 会自动使用它来创建 Web 优化的视频和音频文件。这可以显着减小文件大小并改善用户体验。

FFmpeg 可以从官方 FFmpeg 网站下载。由于该网站只发布源代码,您必须自己编译。为了方便起见,我们已经完成了这部分工作,并将其打包到一个名为 Gallery Server Pro Binary Pack 的免费下载中。尽管我们在此不讨论它们,但该包还包含两个额外的开源工具——ImageMagick 和 GhostScript——它们用于从某些图像和文档类型生成缩略图。

使用 FFmpeg 很简单。例如,可以通过在命令提示符下执行以下命令从 Windows Media File 视频创建 Flash Video 文件:

ffmpeg.exe -i "C:\myvideo.wmv" "C:\myvideo.flv"

FFmpeg 支持大量控制输出视频或音频文件创建方式的选项。在 Gallery Server Pro 中,我们希望创建 H.264 MP4 Web 优化的视频,以便在尽可能多的设备上播放,包括移动设备。我们得出了以下结论:

 ffmpeg.exe -y -i "{SourceFilePath}" -vf "scale=min(iw*min(640/iw\,480/ih)\,iw):min(ih*min(640/iw\,480/ih)\,ih)" -b:v 384k -vcodec libx264 -flags +loop+mv4 -cmp 256 -partitions +parti4x4+parti8x8+partp4x4+partp8x8 -subq 6 -trellis 0 -refs 5 -bf 0 -coder 0 -me_range 16 -g 250 -keyint_min 25 -sc_threshold 40 -i_qfactor 0.71 -qmin 10 -qmax 51 -qdiff 4 -ac 1 -ar 16000 -r 13 -ab 32000 -movflags +faststart "{DestinationFilePath}"

这是一个很难理解的选项,我们不会详细分析整个命令,但让我们看几个关键点。-vf 指定一个过滤器,该过滤器将视频大小调整为大约 640px x 480px,同时保留纵横比。-vcodec 指定 libx264 编解码器。-movflags 使用 faststart 选项,该选项会生成一个视频,在缓冲少量数据后即可播放。没有此标志,必须下载整个视频才能开始播放。

要了解有关这些选项的更多信息,请阅读FFmpeg 文档。Gallery Server Pro 中使用的选项可以在站点管理区域的视频和音频页面上查看和更改。

从 ASP.NET 调用 FFmpeg

正如我们刚才看到的,我们可以使用 FFmpeg 在命令提示符下转码视频或音频文件。要从 ASP.NET 调用它,我们使用 System.Diagnostics.Process:

private void Execute()
{
  bool processCompletedSuccessfully = false;
  
  InitializeOutput();
  
  var info = new ProcessStartInfo(AppSetting.Instance.FFmpegPath, MediaSettings.FFmpegArgs);
  info.UseShellExecute = false;
  info.CreateNoWindow = true;
  info.RedirectStandardError = true;
  info.RedirectStandardOutput = true;
  
  using (Process p = new Process())
  {
    try
    {
      p.StartInfo = info;
      // For some reason, FFmpeg sends data to the ErrorDataReceived event rather than OutputDataReceived.
      p.ErrorDataReceived += ErrorDataReceived;
      //p.OutputDataReceived += new DataReceivedEventHandler(OutputDataReceived);
      p.Start();
  
      p.BeginErrorReadLine();
  
      processCompletedSuccessfully = p.WaitForExit(MediaSettings.TimeoutMs);
  
      if (!processCompletedSuccessfully)
        p.Kill();
  
      p.WaitForExit();
  
      if (!processCompletedSuccessfully || MediaSettings.CancellationToken.IsCancellationRequested)
      {
        File.Delete(MediaSettings.FilePathDestination);
      }
    }
    catch (Exception ex)
    {
      if (!ex.Data.Contains("args"))
      {
        ex.Data.Add("args", MediaSettings.FFmpegArgs);
      }
  
      Events.EventController.RecordError(ex, AppSetting.Instance, MediaSettings.GalleryId, Factory.LoadGallerySettings());
    }
  }
}

/// <summary>
/// Seed the output string builder with any data from a previous conversion of this
/// media object and the basic settings of the conversion.
/// </summary>
private void InitializeOutput()
{
  var item = MediaConversionQueue.Instance.GetCurrentMediaQueueItem();
  if ((item != null) && (item.MediaQueueId == MediaSettings.MediaQueueId))
  {
    // Seed the log with the existing data; this will prevent us from losing the data
    // when we save the output to the media queue instance.
    _output.Append(item.StatusDetail);
  }

  IMediaEncoderSettings mes = MediaSettings.EncoderSetting;
  if (mes != null)
  {
    _output.AppendLine(String.Format("{0} => {1}; {2}", mes.SourceFileExtension, mes.DestinationFileExtension, mes.EncoderArguments));
  }

  _output.AppendLine("Argument String:");
  _output.AppendLine(MediaSettings.FFmpegArgs);
}

/// <summary>
/// Handle the data received event. Collect the command line output and cancel if requested.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.Diagnostics.DataReceivedEventArgs"/> instance 
/// containing the event data.</param>
private void ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
  _output.AppendLine(e.Data);

  var item = MediaConversionQueue.Instance.GetCurrentMediaQueueItem();
  if ((item != null) && (item.MediaQueueId == MediaSettings.MediaQueueId))
  {
    item.StatusDetail = Output;
    // Don't save to database, as the overhead the server/DB chatter it would create is not worth it.
  }

  CancelIfRequested(sender as Process);
}

/// <summary>
/// Kill the FFmpeg process if requested. This will happen when the user deletes a media
/// object that is being processed or deletes the media queue item in the site admin area.
/// </summary>
/// <param name="process">The process running FFmpeg.</param>
private void CancelIfRequested(Process process)
{
  CancellationToken ct = MediaSettings.CancellationToken;
  if (ct.IsCancellationRequested)
  {
    if (process != null)
    {
      try
      {
        process.Kill();
        process.WaitForExit();
      }
      catch (Win32Exception) { }
      catch (SystemException) { }
    }
  }
}

ffmpeg.exe 可执行文件的完整路径和命令行参数会传递给 ProcessStartInfo 实例,然后将其分配给 Process 实例的 StartInfo 属性。当 FFmpeg 处理文件时,它会定期返回数据,这些数据在 ErrorDataReceived 事件中被收集到一个 StringBuilder 中,并最终记录到事件日志。

将超时值传递给 WaitForExit 方法,以便在进程在合理时间内未完成时自动将其关闭。

终止进程

在几种情况下,我们希望在进程自行完成之前将其终止:

  1. 用户删除了正在处理的媒体对象。
  2. 用户删除了站点管理区域“视频和音频”页面上的媒体队列项。

每次 FFmpeg 调用 ErrorDataReceived 事件以提供进度报告时,我们都会检查一个取消令牌,看是否设置了请求取消的标志。如果是,我们调用 Kill 方法,如上面的代码片段所示。

但是,取消令牌是如何分配和更新的?使用 FFmpeg 处理媒体项由一个名为 MediaConversionQueue 的单例类管理。该类有一个名为 CancelTokenSource 的属性:

protected CancellationTokenSource CancelTokenSource { get; set; }

每次我们要求 FFmpeg 进行转换时,我们都会分配此属性,并将其 Token 属性传递给 FFmpeg 类:

CancelTokenSource = new CancellationTokenSource();

var mediaSettings = new MediaConversionSettings
  {
    // ...Other stuff ommitted for clarity...
    CancellationToken = CancelTokenSource.Token
  };

mediaSettings.FFmpegOutput = FFmpeg.CreateMedia(mediaSettings);

这个令牌就是我们在 ErrorDataReceived 事件中检查的内容。当删除媒体对象或删除队列项时,会在令牌源上调用 Cancel 方法(在 MediaConversionQueue.RemoveMediaQueueItem 中):

CancelTokenSource.Cancel();

调用 Cancel 方法会导致后续对 IsCancellationRequested 属性的调用返回 true,从而触发 ErrorDataReceived 事件中的代码来终止进程。

媒体对象、相册和组合模式

每个媒体对象(照片、视频等)都存储在一个相册中。相册可以嵌套在其他相册中,层级数量没有限制。这类似于文件和目录在硬盘上的存储方式。

事实证明,相册和媒体对象有很多共同之处。它们都有诸如 IdTitleDateAddedFullPhysicalPath 等属性;并且它们都有 SaveDelete 等方法。这是使用**组合**设计模式的理想情况,其中公共功能在基对象中定义。我首先定义两个接口 — IGalleryObjectIAlbum

IAlbum 接口继承自 IGalleryObject,然后添加了一些特定于相册的方法和属性。然后我创建了 abstract 基类 GalleryObject。它实现了 IGalleryObject 接口并提供了相册和媒体对象共有的默认行为。例如,这是 DateAdded 属性:

public DateTime DateAdded
{
  get
  {
    VerifyObjectIsInflated(this._dateAdded);
    return this._dateAdded;
  }
  set
  {
    this._hasChanges = (this._dateAdded == value ? this._hasChanges : true);
    this._dateAdded = value;
  }
}

现在,当通用功能在 abstract 基类中定义后,我就可以创建具体的类来表示相册、图像、视频、音频和其他类型的媒体对象了:

Screenshot - galleryobject_classdiagram.jpg

通过这种方法,重复的代码非常少,结构可维护,并且易于使用。例如,当 Gallery Server Pro 想要显示相册中所有对象的标题和缩略图时,其中可能包含任何组合的子相册、图像、视频、音频和其他文档。但我不需要担心所有不同的类或类型转换问题。我只需要以下代码:

// Assume we are loading an album with ID=42

IAlbum album = Factory.LoadAlbumInstance(42, true);
foreach (IGalleryObject galleryObject in album.GetChildGalleryObjects())
{
  string title = galleryObject.Title;
  string thumbnailPath = galleryObject.Thumbnail.FileNamePhysicalPath;
}

很美,不是吗?但是,当功能在两种对象类型之间略有不同时会发生什么?例如,Gallery Server Pro 需要验证每个媒体对象都有一个缩略图,但相册没有缩略图,至少不是严格意义上的。(您看到的相册缩略图实际上是其媒体对象之一的缩略图。)我们希望在基类中定义此验证,以便所有具体类都能继承它,但其中一个具体类 — Album — 需要不同的验证行为。

我们通过首先在 GalleryObject 类中创建验证函数来解决这个问题:

protected virtual void CheckForThumbnailImage()
{
  if (!System.IO.File.Exists(this.Thumbnail.FileNamePhysicalPath))
  {
    this.RegenerateThumbnailOnSave = true;
  }
}

由于它被定义为 protected virtual,我们可以在派生类中覆盖它。事实上,Album 类正是这样做的:

protected override void CheckForThumbnailImage()
{
  // Do nothing
}

最终结果是我们在基类中实现了一个提供大多数情况功能的实现,并且特定于相册的代码包含在 Album 类中。没有重复的代码,逻辑被很好地封装起来。这是一个令人惊叹的景象。

使用策略模式持久化到数据存储

我们刚才看到了当我们需要改变函数的行为时如何覆盖它。当涉及到将相册和媒体对象保存到数据库时,我们也可以做类似的事情。GalleryObject 类中的 Save 方法可以被定义为虚拟的,我们可以在每个派生类中覆盖它。但是,由于 ImageVideoAudioGenericMediaObjectExternalMediaObject 类都代表存储在同一表(MediaObject)中的对象,这意味着需要在所有五个类中编写相同的代码,而只有 Album 类是不同的。

一种消除代码重复问题的方法是在 GalleryObject 类中的 Save 方法中提供默认实现。在该方法中,我将保存到媒体对象表,然后依赖 Album 类来覆盖行为,就像我们对 CheckForThumbnailImage 函数所做的那样。然而,这会将大量行为置于一个不属于它的基类中。我们应该限制基类包含适用于所有派生对象的 state 和 behavior。

您可能会争辩说,当我提供了一个被 Album 类覆盖的 CheckForThumbnailImage 方法的默认实现时,我违反了这条规则。您说得完全正确。但我辩称,在每个派生类中实现验证会产生不良的重复代码,而使用策略模式进行重构又有点过度。这些都不是硬性规定。构建应用程序既是艺术也是科学,您必须权衡每种方法的利弊。

回到我们持久化数据到数据存储的挑战,我们提出的方法是使用**策略**模式来封装行为。首先,我定义了一个接口 ISaveBehavior

public interface ISaveBehavior { void Save(); }

然后我写了两个实现了该接口的类:AlbumSaveBehaviorMediaObjectSaveBehaviorSave 方法负责将对象持久化到硬盘和数据存储。例如,这是 AlbumSaveBehavior 中的 Save 方法:

public void Save()
{
  if (this._albumObject.IsVirtualAlbum)
    return; // Don't save virtual albums.

  // Must save to disk first, since the method queries properties that might 
  // be updated when it is saved to the data store.
  PersistToFileSystemStore(this._albumObject);

  // Save to the data store.
  using (var repo = new AlbumRepository())
  {
    repo.Save(this._albumObject);
  }

  if (this._albumObject.GalleryIdHasChanged)
  {
    // Album has been assigned to a new gallery, so we need to iterate through all
    // its children and update those gallery IDs as well.
    AssignNewGalleryId(this._albumObject.GetChildGalleryObjects(GalleryObjectType.Album), this._albumObject.GalleryId);
  }
}

我们将跳过展示 MediaObjectSaveBehavior 类的实现,但它包含特定于持久化媒体对象的逻辑。

好的,我们有两个用于将数据保存到数据存储的类——一个用于相册,一个用于媒体对象。我们如何在 GalleryObject 基类中调用适当的 Save 方法?

回想一下,GalleryObject 类是 abstract 的,所以它永远不能被直接实例化。相反,我们实例化 AlbumImageVideoAudioGenericMediaObjectExternalMediaObject 类的实例。这些类中的每一个的构造函数都会分配相应的保存行为。例如,在 Album 类的构造函数中,我们有:

this.SaveBehavior = Factory.GetAlbumSaveBehavior(this);

GetAlbumSaveBehavior 方法只是返回 AlbumSaveBehavior 类的一个实例:

public static ISaveBehavior GetAlbumSaveBehavior(IAlbum albumObject)
{
  return new AlbumSaveBehavior(albumObject);
}

GalleryObject 类的 SaveBehavior 属性是 ISaveBehavior 类型。由于两个类都实现了该接口,因此我们可以将任一类的实例分配给该属性。

GalleryObject 类中的 Save 方法只是调用 SaveBehavior 属性上的 Save 方法。它不知道该属性是 AlbumSaveBehavior 还是 MediaObjectSaveBehavior 的实例,它也不关心。所有重要的是每个类都知道如何保存其指定的对象。

这是一个使用策略模式的例子。具体来说,策略模式被定义为一系列算法,这些算法是封装的和可互换的。在我们的例子中,我们有两种保存行为,它们是独立的,并且都可以分配给同一个属性(可互换)。这是一个强大的模式,有很多用途。

摘要

以上简要介绍了 Gallery Server Pro 中使用的架构和编程技术。请随时下载源代码并使用这些代码块来帮助您自己的项目。祝您好运!

文章历史

  • 2013 年 10 月 18 日
    • 为 3.0 版本更新,包含多个新章节
    • 将源代码更新到 3.0.3
  • 2011 年 7 月 1 日
    • 更新了源代码
  • 2011 年 4 月 27 日
    • 将源代码更新到 2.4.7
    • 更新了文章以反映自上次更新以来的变化
  • 2008 年 11 月 3 日
    • 将源代码更新到 2.1.3222
  • 2008 年 9 月 9 日
    • 更新以包含最新的源文件、对新功能的引用以及少量内容更新
  • 2008 年 5 月 6 日
    • 更新以包含最新的源文件和少量内容更新
  • 2007 年 10 月 28 日
    • 文章发布
© . All rights reserved.