ThinkPHP5实战:阿里云OSS配合MPS媒体处理实现图片/视频动态文字水印

在很多应用场景中,用户上传的图片或视频需要打上专属的水印(如版权声明、拍摄地点、时间等),以防止盗用并标明出处。本文将基于 ThinkPHP5(FastAdmin)框架,演示如何利用 阿里云OSS(对象存储) 以及 MPS(媒体处理服务) ,实现图片和视频在上传后自动添加文字水印的完整方案。

一、 业务场景与需求

1. 核心需求

  • 多端支持 :客户端(App/小程序/H5)通过接口将文件上传至服务端。
  • 自适应判断 :服务端根据上传的文件类型,自动判断是图片还是视频。
  • 图片处理(OSS) :如果是图片,直接利用阿里云 OSS 的图片处理能力,拼接 URL 参数( x-oss-process ),实现 实时 图文水印。
  • 视频处理(MPS) :如果是视频,由于视频水印无法实时渲染,需要调用阿里云 MPS 媒体处理服务,提交一个 异步转码任务 。在原视频的基础上烧录文字水印,并生成一个新的带水印的 MP4 文件。
  • 自定义参数 :支持前端传入自定义的水印文字(支持两行,如地点+时间)、颜色、大小以及显示位置(左上、右上、左下、右下)。

2. 阿里云前置配置

在实现代码前,需要确保你已经开通了以下阿里云服务,并获取了关键参数:

  1. AccessKey 权限 :需要一个拥有 AliyunOSSFullAccess 和 AliyunMTSFullAccess 权限的子账号 AccessKey。
  2. MPS 管道 ID ( PipelineId ) :进入 MPS 控制台 -> 全局设置 -> 管道及回调,创建一个处理队列,拿到 32 位的 PipelineId。
  3. MPS 转码模板 ID ( TemplateId ) :可以使用系统预置模板(例如输出标准 MP4 格式的 S00000001-200020 ),也可以自定义模板。

二、 完整核心代码实现

以下是在 ThinkPHP5 API 控制器中的完整代码实现,包含了文件上传、图片水印参数组装以及 MPS 视频水印转码任务的提交逻辑。

php 复制代码
<?php
namespace app\api\controller;

use app\common\controller\Api;
use app\common\exception\UploadException;
use app\common\library\Upload;
use OSS\OssClient;
use AlibabaCloud\Client\AlibabaCloud;
use AlibabaCloud\Client\Exception\ClientException;
use AlibabaCloud\Client\Exception\ServerException;
use think\Log;

class OssUpload extends Api
{ 
    protected $noNeedLogin = ['upload'];
    protected $noNeedRight = ['*'];

    /**
     * 上传文件并添加文字水印(图片实时处理 / 视频MPS异步转码)
     */ 
    public function upload()
    {     
        // 1. 接收前端传入的水印参数
        $watermark_text = $this->request->post('watermark_text', '');
        $watermark_address = $this->request->post('watermark_address', '');
        $watermark_time = $this->request->post('watermark_time', '');
        $watermark_size = 30; // 固定或接收传入
        
        $watermark_color = $this->request->post('watermark_color', 'FF0000');
        $watermark_color = strtoupper(ltrim($watermark_color, '#')); // 去除#号
        $watermark_position = $this->request->post('watermark_position', 'right_bottom');
        
        $video_watermark_address = $watermark_address ?: $watermark_text;
        $video_watermark_time = $watermark_time;
        
        // 获取 FastAdmin 插件配置中的 OSS 参数
        $config = get_addon_config('alioss');
        if (!$config) {
            $this->error('未配置或未安装阿里云OSS插件');
        }

        try {
            $oss = new OssClient($config['accessKeyId'], $config['accessKeySecret'], $config['endpoint']);
        } catch (\Exception $e) {
            $this->error('OSS客户端初始化失败');
        }

        // 2. 处理本地文件上传
        $file = $this->request->file('file');
        if (!$file) {
            $this->error('未上传文件');
        }
        try {
            $upload = new Upload($file);
            $attachment = $upload->upload();
        } catch (UploadException $e) {
            $this->error($e->getMessage());
        }

        $filePath = $upload->getFile()->getRealPath() ?: $upload->getFile()->getPathname();
        $url = $attachment->url;
        $objectKey = ltrim($url, "/");

        // 3. 将本地文件推送到阿里云 OSS
        try {
            $oss->uploadFile($config['bucket'], $objectKey, $filePath);
        } catch (\Exception $e) {
            Log::record('文件推送到OSS失败: ' . $e->getMessage(), 'error');
            $this->error("上传失败:" . $e->getMessage());
        }

        $fullurl = cdnurl($url, true);
        $isImage = in_array(strtolower($attachment->imagetype), ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']);
        $isVideo = in_array(strtolower($attachment->imagetype), ['mp4', 'avi', 'flv', 'mov', 'mkv']);

        // ==========================================
        // 场景 A:处理图片水印(实时 URL 拼接)
        // ==========================================
        if ($isImage) {
            $imageWatermarkParams = [];
            // 九宫格定位映射
            $imagePositionMap = [
                'left_top' => 'nw', 'right_top' => 'ne',
                'left_bottom' => 'sw', 'right_bottom' => 'se',
            ];
            $g_pos = isset($imagePositionMap[$watermark_position]) ? $imagePositionMap[$watermark_position] : 'sw';

            if ($watermark_address || $watermark_time) {
                // 注意:图片水印的文字必须进行 URL 安全的 Base64 编码
                if ($watermark_address) {
                    $encodedAddress = rtrim(strtr(base64_encode($watermark_address), '+/', '-_'), '=');
                    $imageWatermarkParams[] = 'watermark,text_' . $encodedAddress . ',color_' . $watermark_color . ',size_' . $watermark_size . ',g_' . $g_pos . ',x_10,y_40,t_90';
                }
                if ($watermark_time) {
                    $encodedTime = rtrim(strtr(base64_encode($watermark_time), '+/', '-_'), '=');
                    $imageWatermarkParams[] = 'watermark,text_' . $encodedTime . ',color_' . $watermark_color . ',size_26,g_' . $g_pos . ',x_10,y_10,t_90';
                }
            }

            // 拼接 OSS 图片处理参数
            if (!empty($imageWatermarkParams)) {
                $watermarkParam = '?x-oss-process=image/' . implode('/', $imageWatermarkParams);
                $url .= $watermarkParam;
                $fullurl .= $watermarkParam;
            }
        }

        // ==========================================
        // 场景 B:处理视频水印(提交 MPS 转码任务)
        // ==========================================
        if (($video_watermark_address || $video_watermark_time) && $isVideo) {
            $pipelineId = '7beb974xxxxxx'; // 替换为你的管道 ID
            
            // 提取区域 ID,例如 cn-shanghai
            $regionId = 'cn-shanghai';
            if (preg_match('/oss-(.*?)\./', $config['endpoint'], $matches)) {
                $regionId = $matches[1];
            }
            $location = 'oss-' . $regionId;

            // 定义带水印输出的新文件名
            $newObjectKey = str_replace('.' . $attachment->imagetype, '_watermark.mp4', $objectKey);
            $newUrl = '/' . $newObjectKey;
            $newFullurl = cdnurl($newUrl, true);
            
            // MPS 要求颜色为英文单词
            $videoFontColorMap = [
                'FFFFFF' => 'White', '000000' => 'Black', 'FF0000' => 'Red',
            ];
            $videoFontColor = isset($videoFontColorMap[$watermark_color]) ? $videoFontColorMap[$watermark_color] : 'Red';
            
            // 视频水印位置使用相对百分比定位
            $videoWatermarkPositionMap = [
                'left_bottom' => [
                    'address' => ['Top' => '0.76', 'Left' => '0.03'],
                    'time'    => ['Top' => '0.84', 'Left' => '0.03'],
                ],
                'right_bottom' => [
                    'address' => ['Top' => '0.76', 'Left' => '0.58'],
                    'time'    => ['Top' => '0.84', 'Left' => '0.58'],
                ],
            ];
            $videoWatermarkPosition = isset($videoWatermarkPositionMap[$watermark_position]) ? $videoWatermarkPositionMap[$watermark_position] : $videoWatermarkPositionMap['right_bottom'];
            
            // 组装水印数组
            $videoWaterMarks = [];
            if ($video_watermark_address) {
                $videoWaterMarks[] = [
                    'Type'          => 'Text',
                    'TextWaterMark' => [
                        'Content'   => base64_encode($video_watermark_address), // 视频水印普通 Base64 即可
                        'FontName'  => 'SimSun',
                        'FontSize'  => $watermark_size,
                        'FontColor' => $videoFontColor,
                        'FontAlpha' => 1,
                        'Top'       => $videoWatermarkPosition['address']['Top'],
                        'Left'      => $videoWatermarkPosition['address']['Left']
                    ]
                ];
            }

            try {
                // 初始化 MPS 客户端
                AlibabaCloud::accessKeyClient($config['accessKeyId'], $config['accessKeySecret'])
                    ->regionId($regionId)
                    ->asDefaultClient();

                $outputs = [    
                    [
                        'OutputObject' => urlencode($newObjectKey), // 文件名必须 urlencode
                        'TemplateId'   => 'S00000001-200020',
                        'WaterMarks'   => $videoWaterMarks
                    ]
                ];

                // 提交 MPS 转码作业 (SubmitJobs)
                $result = AlibabaCloud::rpc()
                    ->product('Mts')
                    ->version('2014-06-18')
                    ->action('SubmitJobs')
                    ->method('POST')
                    ->host("mts.{$regionId}.aliyuncs.com")
                    ->options([
                        'query' => [
                            'PipelineId'     => $pipelineId,
                            'Input'          => json_encode(['Bucket' => $config['bucket'], 'Location' => $location, 'Object' => urlencode($objectKey)], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                            'OutputBucket'   => $config['bucket'],
                            'OutputLocation' => $location,
                            'Outputs'        => json_encode($outputs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                        ],
                    ])
                    ->request();

                Log::record('MPS任务提交成功: ' . json_encode($result->toArray(), JSON_UNESCAPED_UNICODE), 'info');

                // 替换 URL 为最终水印视频的 URL
                $url = $newUrl;
                $fullurl = $newFullurl;
                
                // 更新数据库中的记录
                $attachment->url = $url;
                $attachment->save();

            } catch (\Exception $e) {
                Log::record('MPS异常: ' . $e->getMessage(), 'error');
            }
        }

        $this->success("上传成功", ['url' => $url, 'fullurl' => $fullurl]);
    }
}