0
0
0

运营版uniapp多商户商城小程序/种草/公众号/h5/app/商家入驻/在线客服/秒杀/拼团/砍价

职场区 3253582893 1月前
1560

源码获取:ym7k.com/17882/

 

多商户商城直播系统:全终端观看与推流限制的工程突破

引子:直播的技术门槛不在播放,而在"全终端"

“全终端观看直播,H5 外所有终端支持推流”——这句话看起来只是一个功能描述,但它触及了多商户商城系统中最难啃的技术骨头。

直播的播放和推流,在不同终端上的技术路径完全不同。小程序有微信的推流组件限制,App 需要集成原生推流 SDK,H5 的直播播放依赖浏览器对 HLS/FLV 的支持程度,公众号受限于微信内置浏览器。要让一套系统在所有这些终端上都能正常观看,并且让商家在除 H5 外的所有终端上都能推流,需要解决一系列工程问题。

更重要的是,这是一个多商户系统。每个商户都需要独立的直播间、独立的推流地址、独立的商品列表、独立的订单归属。直播不是单商户的功能,而是多租户架构下的实时业务。

本文从多商户直播的架构设计、全终端播放的兼容方案、App 与小程序推流的技术实现、直播与商城的业务耦合,到直播的存储与回放,展开这类系统的完整技术分析。

一、多商户直播的架构设计

1.1 直播系统的三层结构

多商户直播系统的架构可以抽象为三层。

流媒体层。 负责音视频流的接收(推流)、转码、分发(拉流)。核心组件是 RTMP 服务器、转码服务和 CDN。

业务层。 负责直播间的创建、状态管理、商品关联、互动消息、订单归因。这是商城系统与流媒体层的交界面。

终端层。 负责推流和播放。不同终端使用不同的 SDK 和协议。

三层之间的数据流是:主播通过终端推流 → 流媒体层接收并转码 → 业务层记录直播间状态 → 观众通过终端拉流 → 业务层处理互动和订单。

1.2 多租户隔离

多商户系统要求每个商户的直播间完全隔离。隔离维度包括:推流地址隔离(每个商户不同的推流密钥)、播放地址隔离(不同的直播间不同的播放地址)、商品隔离(直播间只能挂本商户的商品)、订单隔离(直播订单归属于对应的商户)。

sql
CREATE TABLE `live_room` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `merchant_id` int(11) NOT NULL COMMENT '所属商户',
  `title` varchar(200) NOT NULL,
  `cover` varchar(255) NOT NULL,
  `stream_key` varchar(64) NOT NULL COMMENT '推流密钥',
  `push_url` varchar(500) DEFAULT NULL COMMENT '推流地址',
  `play_url_flv` varchar(500) DEFAULT NULL COMMENT 'HTTP-FLV 播放地址',
  `play_url_hls` varchar(500) DEFAULT NULL COMMENT 'HLS 播放地址',
  `play_url_rtmp` varchar(500) DEFAULT NULL COMMENT 'RTMP 播放地址',
  `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=预告 1=直播中 2=已结束',
  `scheduled_at` datetime DEFAULT NULL COMMENT '预告开播时间',
  `started_at` datetime DEFAULT NULL,
  `ended_at` datetime DEFAULT NULL,
  `viewer_count` int(11) NOT NULL DEFAULT '0',
  `peak_viewer` int(11) NOT NULL DEFAULT '0',
  `like_count` int(11) NOT NULL DEFAULT '0',
  `replay_url` varchar(500) DEFAULT NULL COMMENT '回放地址',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_stream_key` (`stream_key`),
  KEY `idx_merchant_status` (`merchant_id`, `status`),
  KEY `idx_status_scheduled` (`status`, `scheduled_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

stream_key 是推流的核心凭证。它是一个唯一字符串,主播推流时需要提供这个密钥,服务器据此识别是哪个直播间。merchant_id 保证了每个直播间归属于特定的商户。

1.3 流媒体服务器的选型

对于多商户商城的私有化部署,流媒体服务器的选择有三个方案。

方案一:SRS(Simple Realtime Server)。 开源、轻量、支持 RTMP 推流和 HLS/HTTP-FLV 拉流。部署简单,适合中小型平台。

方案二:Nginx-rtmp-module。 基于 Nginx 的 RTMP 模块,配置灵活,但功能相对基础,不支持 HTTP-FLV 的低延迟播放。

方案三:云直播服务。 阿里云直播、腾讯云直播、声网等。优点是稳定、低延迟、全球覆盖;缺点是成本随流量线性增长,数据不自主可控。

对于私有化部署的多商户商城,SRS 是最务实的选择。它支持 RTMP 推流、HLS/HTTP-FLV 拉流,且有完善的 HTTP API 支持直播间管理。

二、全终端播放的兼容方案

2.1 各终端的播放能力差异

直播播放的核心是协议和播放器的选择。不同终端对协议的支持程度不同。

微信小程序。 支持 live-player 组件播放 RTMP 和 HTTP-FLV,但需要开通小程序的直播权限(类目审核)。HLS 在小程序中延迟较高(10-30秒),HTTP-FLV 延迟较低(3-5秒)。

App。 可以使用原生播放器(如 ijkplayer、ExoPlayer)或 UniApp 的原生插件。支持 RTMP、HTTP-FLV、HLS 全协议。

H5。 浏览器的支持最受限。Safari 原生支持 HLS,Chrome 需要 hls.js 库。HTTP-FLV 需要 flv.js 库。RTMP 在浏览器中已不支持(Flash 已废弃)。

公众号。 本质上是 H5 运行在微信内置浏览器中,播放能力与 H5 相同。

2.2 播放协议的降级策略

由于各终端的支持程度不同,系统需要提供协议降级:优先使用低延迟协议(HTTP-FLV),不支持时降级到 HLS。

javascript
// utils/live/player.js

/**
 * 根据终端和协议支持情况,选择合适的播放地址
 */
export function selectPlayUrl(room) {
  // #ifdef MP-WEIXIN
  // 小程序优先使用 HTTP-FLV(低延迟)
  return room.play_url_flv || room.play_url_hls;
  // #endif

  // #ifdef APP-PLUS
  // App 优先使用 HTTP-FLV,也支持 RTMP
  return room.play_url_flv || room.play_url_rtmp || room.play_url_hls;
  // #endif

  // #ifdef H5
  // H5 根据浏览器能力选择
  if (supportsFLV()) {
    return room.play_url_flv;
  }
  if (supportsHLS()) {
    return room.play_url_hls;
  }
  return room.play_url_hls; // 兜底
  // #endif
}

function supportsFLV() {
  // #ifdef H5
  // 检查浏览器是否支持 MSE(Media Source Extensions)
  // flv.js 依赖 MSE
  return typeof window.MediaSource !== 'undefined'
    && window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E"');
  // #endif
  return false;
}

function supportsHLS() {
  // #ifdef H5
  const video = document.createElement('video');
  return video.canPlayType('application/vnd.apple.mpegurl') !== '';
  // #endif
  return false;
}

2.3 H5 端的播放器实现

H5 端的直播播放是最复杂的,因为需要根据浏览器能力动态加载播放器。

vue
<!-- components/LivePlayer.vue -->
<template>
  <view class="live-player">
    <!-- 小程序端使用 live-player 组件 -->
    <!-- #ifdef MP-WEIXIN -->
    <live-player
      :src="playUrl"
      mode="live"
      autoplay
      @statechange="onStateChange"
      @error="onError"
    />
    <!-- #endif -->

    <!-- H5 端使用 video 标签 + 播放器库 -->
    <!-- #ifdef H5 -->
    <video
      ref="videoEl"
      class="h5-player"
      controls
      autoplay
      playsinline
    />
    <!-- #endif -->

    <!-- App 端使用原生播放器 -->
    <!-- #ifdef APP-PLUS -->
    <view class="app-player">
      <!-- App 端通过原生插件或 video 组件播放 -->
      <video
        :src="playUrl"
        autoplay
        controls
      />
    </view>
    <!-- #endif -->
  </view>
</template>

<script>
export default {
  props: {
    room: Object,
  },
  data() {
    return {
      playUrl: '',
      flvPlayer: null,
      hlsPlayer: null,
    };
  },
  mounted() {
    this.playUrl = this.selectUrl();
    this.initPlayer();
  },
  beforeDestroy() {
    this.destroyPlayer();
  },
  methods: {
    selectUrl() {
      // #ifdef MP-WEIXIN
      return this.room.play_url_flv || this.room.play_url_hls;
      // #endif

      // #ifdef APP-PLUS
      return this.room.play_url_flv || this.room.play_url_rtmp || this.room.play_url_hls;
      // #endif

      // #ifdef H5
      if (this.supportsFLV() && this.room.play_url_flv) {
        return this.room.play_url_flv;
      }
      return this.room.play_url_hls;
      // #endif
    },

    supportsFLV() {
      // #ifdef H5
      return typeof window.MediaSource !== 'undefined';
      // #endif
      return false;
    },

    async initPlayer() {
      // #ifdef H5
      await this.initH5Player();
      // #endif
    },

    async initH5Player() {
      const videoEl = this.$refs.videoEl;
      if (!videoEl) return;

      if (this.playUrl.includes('.flv')) {
        // 动态加载 flv.js
        const flvjs = await import('flv.js');
        if (flvjs.default.isSupported()) {
          this.flvPlayer = flvjs.default.createPlayer({
            type: 'flv',
            url: this.playUrl,
            isLive: true,
            hasAudio: true,
          });
          this.flvPlayer.attachMediaElement(videoEl);
          this.flvPlayer.load();
          this.flvPlayer.play();
        }
      } else if (this.playUrl.includes('.m3u8')) {
        // 动态加载 hls.js(Safari 原生支持 HLS,不需要)
        if (!videoEl.canPlayType('application/vnd.apple.mpegurl')) {
          const Hls = await import('hls.js');
          if (Hls.default.isSupported()) {
            this.hlsPlayer = new Hls.default();
            this.hlsPlayer.loadSource(this.playUrl);
            this.hlsPlayer.attachMedia(videoEl);
          }
        } else {
          videoEl.src = this.playUrl;
        }
      }
    },

    destroyPlayer() {
      if (this.flvPlayer) {
        this.flvPlayer.destroy();
        this.flvPlayer = null;
      }
      if (this.hlsPlayer) {
        this.hlsPlayer.destroy();
        this.hlsPlayer = null;
      }
    },

    onStateChange(e) {
      // 小程序播放状态变化
    },

    onError(e) {
      console.error('播放错误', e);
    },
  },
};
</script>

这个播放器组件的核心设计是动态加载播放器库 + 协议降级。H5 端根据播放地址的格式(.flv 或 .m3u8)动态加载 flv.js 或 hls.js,而不是在初始包中引入所有播放器。这降低了首屏加载时间。

三、推流:App 与小程序的技术实现

3.1 推流的技术门槛

推流比播放的技术门槛更高。播放只需要一个播放器,推流需要采集音视频、编码、封装、推送。

音频采集。 需要访问麦克风,采集 PCM 音频,编码为 AAC。

视频采集。 需要访问摄像头,采集 YUV 视频帧,编码为 H.264。

封装。 将音频和视频封装为 FLV 或 RTMP 格式。

推送。 通过 RTMP 协议推送到流媒体服务器。

在 UniApp 中,小程序和 App 的推流实现方式完全不同。

3.2 微信小程序推流

微信小程序提供了 live-pusher 组件用于推流。它封装了采集、编码和推送的全流程。

vue
<!-- pages/live/push.vue -->
<template>
  <view class="push-page">
    <!-- #ifdef MP-WEIXIN -->
    <live-pusher
      ref="pusher"
      :url="pushUrl"
      mode="HD"
      :aspect="aspect"
      :beauty="beauty"
      :whiteness="whiteness"
      :muted="muted"
      :enable-camera="enableCamera"
      @statechange="onPushStateChange"
      @error="onPushError"
    />
    <!-- #endif -->
  </view>
</template>

<script>
export default {
  data() {
    return {
      pushUrl: '',
      aspect: '9:16',
      beauty: 5,
      whiteness: 5,
      muted: false,
      enableCamera: true,
    };
  },
  async onLoad(options) {
    // 创建直播间或获取已有直播间的推流地址
    const res = await this.$api.live.createRoom({
      title: options.title || '直播',
    });
    this.pushUrl = res.data.push_url;
  },
  methods: {
    onPushStateChange(e) {
      // 推送状态变化
      const code = e.detail.code;
      if (code === 1002) {
        // 已开始推流,通知后端更新直播间状态
        this.$api.live.start(this.roomId);
      }
    },
    onPushError(e) {
      console.error('推流错误', e);
    },
    startPush() {
      // #ifdef MP-WEIXIN
      this.$refs.pusher.start();
      // #endif
    },
    stopPush() {
      // #ifdef MP-WEIXIN
      this.$refs.pusher.stop();
      // #endif
    },
  },
};
</script>

小程序推流的工程约束是:需要开通小程序的直播权限。微信对 live-pusher 和 live-player 组件有类目限制,只有特定类目(如电商、教育、医疗)的小程序才能使用。这是多商户系统需要处理的问题——每个商户的小程序都需要单独申请权限。

3.3 App 端推流

App 端的推流需要集成原生推流 SDK。UniApp 没有内置推流组件,需要使用原生插件。

常见的推流 SDK 包括:腾讯云 MLVB SDK、阿里云推流 SDK、声网 Agora、ijkplayer(仅播放)。

javascript
// utils/live/appPusher.js

/**
 * App 端推流封装
 * 依赖原生插件:LivePusher
 */
class AppPusher {
  constructor() {
    this.pusher = null;
  }

  init(options) {
    // #ifdef APP-PLUS
    this.pusher = uni.requireNativePlugin('LivePusher');

    this.pusher.init({
      url: options.pushUrl,
      mode: options.mode || 'HD',
      aspect: options.aspect || '9:16',
      beauty: options.beauty || 5,
      whiteness: options.whiteness || 5,
      audioQuality: 'high',
      videoQuality: 'high',
    }, (result) => {
      if (result.code === 0) {
        console.log('推流初始化成功');
      } else {
        console.error('推流初始化失败', result);
      }
    });
    // #endif
  }

  start() {
    // #ifdef APP-PLUS
    if (this.pusher) {
      this.pusher.start();
    }
    // #endif
  }

  stop() {
    // #ifdef APP-PLUS
    if (this.pusher) {
      this.pusher.stop();
    }
    // #endif
  }

  switchCamera() {
    // #ifdef APP-PLUS
    if (this.pusher) {
      this.pusher.switchCamera();
    }
    // #endif
  }

  destroy() {
    // #ifdef APP-PLUS
    if (this.pusher) {
      this.pusher.destroy();
      this.pusher = null;
    }
    // #endif
  }
}

export const appPusher = new AppPusher();

3.4 H5 端不支持推流的原因

H5 端在浏览器中运行,无法直接访问设备的摄像头和麦克风进行 RTMP 推流。虽然浏览器提供了 getUserMedia API 访问摄像头和麦克风,但浏览器无法直接将音视频流推送到 RTMP 服务器。

浏览器的推流方案是 WebRTC,但 WebRTC 需要服务端支持,且与 RTMP 不兼容。如果系统需要支持 H5 推流,需要额外的 WebRTC 服务端和协议转换。

对于多商户商城系统,H5 端不做推流是合理的选择。商家用 App 或小程序推流,观众用任意终端观看,这已经覆盖了绝大多数使用场景。

四、直播与商城的业务耦合

4.1 直播间的商品管理

直播间与商品的关联是直播带货的核心。主播在直播过程中可以切换讲解的商品,观众在直播间看到当前讲解的商品并直接下单。

sql
CREATE TABLE `live_product` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `room_id` int(11) NOT NULL,
  `product_id` int(11) NOT NULL,
  `sort` tinyint(4) NOT NULL DEFAULT '0',
  `is_active` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否正在讲解',
  `active_at` datetime DEFAULT NULL,
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_room_product` (`room_id`, `product_id`),
  KEY `idx_room_sort` (`room_id`, `sort`),
  KEY `idx_active` (`room_id`, `is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

is_active 字段标记当前正在讲解的商品。同一时刻,一个直播间只有一个商品是 is_active = 1。当主播切换商品时,系统更新这个字段,并通过 WebSocket 推送消息给所有观众,观众的界面自动滚动到当前讲解的商品。

4.2 直播订单的归因

直播订单需要归因到具体的直播间和主播。这个归因用于计算主播的佣金和商户的直播效果分析。

sql
ALTER TABLE `order` ADD COLUMN `live_room_id` int(11) DEFAULT NULL 
  COMMENT '直播订单归因的直播间ID' AFTER `merchant_id`;
ALTER TABLE `order` ADD KEY `idx_live_room` (`live_room_id`);

当用户在直播间下单时,订单记录 live_room_id。这样商户和主播可以看到:本场直播带来了多少订单、多少 GMV。

4.3 实时互动:弹幕、点赞、下单通知

直播间的实时互动通过 WebSocket 实现。弹幕、点赞、下单通知都需要实时推送给所有观众。

php
<?php
// live_server.php — 基于 Workerman 的直播 WebSocket 服务

use Workerman\Worker;
use Workerman\Connection\TcpConnection;

$ws = new Worker('websocket://0.0.0.0:8282');
$ws->count = 4;

$rooms = [];

$ws->onConnect = function (TcpConnection $connection) {
    $connection->roomId = null;
    $connection->userId = null;
};

$ws->onMessage = function (TcpConnection $connection, $data) use (&$rooms) {
    $msg = json_decode($data, true);
    if (!$msg || !isset($msg['type'])) return;

    switch ($msg['type']) {
        case 'join':
            $roomId = (int)$msg['room_id'];
            $connection->roomId = $roomId;
            $connection->userId = (int)$msg['user_id'];

            if (!isset($rooms[$roomId])) {
                $rooms[$roomId] = [];
            }
            $rooms[$roomId][$connection->id] = $connection;

            broadcast($rooms[$roomId], [
                'type' => 'viewer_count',
                'count' => count($rooms[$roomId]),
            ]);
            break;

        case 'danmaku':
            if ($connection->roomId) {
                broadcast($rooms[$connection->roomId] ?? [], [
                    'type' => 'danmaku',
                    'user_id' => $connection->userId,
                    'content' => mb_substr($msg['content'], 0, 50),
                    'timestamp' => time(),
                ]);
            }
            break;

        case 'like':
            if ($connection->roomId) {
                broadcast($rooms[$connection->roomId] ?? [], [
                    'type' => 'like',
                    'user_id' => $connection->userId,
                    'count' => $msg['count'] ?? 1,
                ]);
            }
            break;

        case 'product_active':
            // 主播切换讲解商品(仅主播可操作)
            if ($connection->roomId) {
                broadcast($rooms[$connection->roomId] ?? [], [
                    'type' => 'product_active',
                    'product_id' => (int)$msg['product_id'],
                ]);
            }
            break;

        case 'order_notify':
            // 下单通知(由后端订单服务触发)
            if ($connection->roomId) {
                broadcast($rooms[$connection->roomId] ?? [], [
                    'type' => 'order_notify',
                    'nickname' => $msg['nickname'],
                    'product_name' => $msg['product_name'],
                ]);
            }
            break;
    }
};

$ws->onClose = function (TcpConnection $connection) use (&$rooms) {
    if ($connection->roomId && isset($rooms[$connection->roomId])) {
        unset($rooms[$connection->roomId][$connection->id]);
        if (empty($rooms[$connection->roomId])) {
            unset($rooms[$connection->roomId]);
        } else {
            broadcast($rooms[$connection->roomId], [
                'type' => 'viewer_count',
                'count' => count($rooms[$connection->roomId]),
            ]);
        }
    }
};

function broadcast(array $connections, array $message) {
    $json = json_encode($message);
    foreach ($connections as $conn) {
        $conn->send($json);
    }
}

Worker::runAll();

这个 WebSocket 服务的核心设计是房间隔离。每个直播间是一个独立的房间,消息只在房间内广播。这保证了不同商户的直播间不会互相干扰。

五、直播回放与存储

5.1 回放的生成

直播结束后,流媒体服务器可以自动将直播流录制为视频文件。SRS 支持通过配置开启录制,录制文件保存为 FLV 或 MP4。

nginx
# SRS 配置:开启录制
vhost __defaultVhost__ {
    dvr {
        enabled on;
        dvr_path ./objs/nginx/html/[app]/[stream].[timestamp].flv;
        dvr_plan session;  # 按会话录制
    }
}

录制完成后,系统需要将回放地址写入直播间记录,并触发转码(将 FLV 转为 HLS 以便在 H5 端播放)。

php
<?php
// app/common/service/LiveReplayService.php

namespace app\common\service;

use think\facade\Db;
use think\facade\Queue;

class LiveReplayService
{
    /**
     * 直播结束后处理回放
     */
    public function handleReplay(int $roomId, string $recordPath): void
    {
        // 异步转码
        Queue::push('app\job\TranscodeReplay', [
            'room_id' => $roomId,
            'record_path' => $recordPath,
        ], 'video');

        Db::name('live_room')
            ->where('id', $roomId)
            ->update([
                'status' => 2,
                'ended_at' => date('Y-m-d H:i:s'),
                'replay_record_path' => $recordPath,
            ]);
    }

    /**
     * 转码回放(队列消费者)
     */
    public function transcode(array $data): void
    {
        $roomId = $data['room_id'];
        $source = $data['record_path'];

        // 转码为 HLS(多码率)
        $outputDir = "/var/www/replay/{$roomId}/";
        if (!is_dir($outputDir)) {
            mkdir($outputDir, 0755, true);
        }

        $cmd = sprintf(
            'ffmpeg -i %s -c:v libx264 -preset medium -b:v 1500k '
            . '-c:a aac -b:a 128k -f hls -hls_time 10 '
            . '-hls_list_size 0 -hls_segment_filename "%s/seg_%%03d.ts" '
            . '%s/index.m3u8 2>&1',
            escapeshellarg($source),
            $outputDir,
            $outputDir
        );

        exec($cmd, $output, $returnCode);

        if ($returnCode !== 0) {
            \think\facade\Log::error('回放转码失败', [
                'room_id' => $roomId,
                'output' => implode("\n", $output),
            ]);
            return;
        }

        // 更新回放地址
        $replayUrl = "/replay/{$roomId}/index.m3u8";
        Db::name('live_room')
            ->where('id', $roomId)
            ->update(['replay_url' => $replayUrl]);
    }
}

5.2 回放的播放

回放的播放比直播简单,因为它不需要低延迟。HLS 是回放的首选协议,因为它在所有终端上的兼容性最好。

javascript
// 回放播放地址选择
export function selectReplayUrl(room) {
  // 所有终端统一使用 HLS
  return room.replay_url;
}

六、边界与张力

多商户商城直播系统在工程实践中,有几个需要清醒认识的边界。

第一,小程序的直播权限限制。 微信对 live-pusher 和 live-player 组件有类目限制。不是所有商户的小程序都能开通直播权限。多商户系统需要提供"直播功能降级"方案:没有直播权限的小程序只能观看 H5 端的直播(通过 web-view 嵌入),或者只能观看回放。

第二,推流的带宽成本。 推流消耗上行带宽,拉流消耗下行带宽。一场 1000 人观看的直播,按 720p 码率(1.5Mbps)计算,每分钟消耗约 11GB 下行流量。多商户平台的带宽成本会随直播场次和观众数快速累积。

第三,直播延迟与互动的矛盾。 HTTP-FLV 的延迟约 3-5 秒,HLS 的延迟约 10-30 秒。观众看到的画面和主播的实际动作有时间差。这导致互动(如"主播看这里")有时效性问题。如果要降低延迟,需要使用 WebRTC,但 WebRTC 的服务器成本和复杂度更高。

第四,多商户的内容审核。 直播是实时内容,审核难度远高于图文。系统需要提供实时审核能力(AI 识别违规画面 + 人工抽检),以及紧急关停机制。

第五,直播录制的存储成本。 每场直播的录制文件可能达到数 GB。多商户平台需要制定存储策略:保留多久、是否转码、是否清理。对象存储(如 S3、OSS)是比本地磁盘更合适的选择。

结语:全终端直播的工程本质是"协议适配 + 能力降级"

多商户商城直播系统的核心工程挑战,可以归结为一句话:在有限的终端能力下,提供最接近一致的直播体验。

播放侧,系统需要根据终端能力选择协议:小程序和 App 优先 HTTP-FLV(低延迟),H5 根据浏览器能力选择 FLV 或 HLS。播放器动态加载,避免首屏负担。

推流侧,小程序使用 live-pusher 组件,App 使用原生推流 SDK,H5 不支持推流。这是终端能力的客观限制,工程上的应对是"能力降级"而非"强行突破"。

业务侧,直播间、商品、订单、互动消息都需要与多商户架构正确耦合。每个商户的直播间是独立的,商品是隔离的,订单是归属明确的。

这套系统的价值,最终体现在一个朴素的判断上:一个商户用 App 开播,观众用小程序、H5 或 App 都能看到直播并下单,直播结束后回放可以随时观看。 这个"都能看到"就是全终端直播的工程底线。

最新回复 (0)

    暂无评论

请先登录后发表评论!

返回
请先登录后发表评论!