npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@oryzateam/hevc-player

v0.4.8

Published

Trình phát HEVC/H.265 cho web: libavcodec WASM thường trú, WebGL trong worker, hỗ trợ MoQ/HLS/MP4

Readme

@oryzateam/hevc-player

Trình phát HEVC/H.265 cho web, dựng thành React component.

Giải mã bằng libavcodec WASM thường trú và vẽ bằng WebGL trong worker, tự dùng <video> native khi nguồn là H.264 (hoặc khi máy giải mã được HEVC bằng phần cứng). Nguồn hỗ trợ: MoQ (Media-over-QUIC của MediaMTX), HLS, và MP4 trực tiếp — kèm seek tới giây N và tự phục hồi khi lỗi mạng tạm thời.

Không phụ thuộc Tailwind; style tự chứa.

Cài đặt

npm install @oryzateam/hevc-player

react/react-dom là peer dependency.

1. Chép lõi giải mã

Player nạp lõi WASM và hai worker tại /hevc/... lúc chạy. Thêm vào package.json của app:

{
  "scripts": {
    "predev": "hevc-copy-assets public",
    "prebuild": "hevc-copy-assets public"
  }
}

Thư mục kết quả (1.8 MB):

public/hevc/
├── libavcodec/        lõi WASM 512MB — nguồn 4K, một luồng
├── libavcodec-grid/   lõi WASM 64MB  — ô lưới, tới 1080p
└── stream/            worker giải mã + worker render

Chép chứ không để bundler xử lý, vì đây là worker + WASM: chúng phải là file riêng, cùng origin, và worker tự nạp lẫn nhau bằng đường dẫn thật. Muốn host ở chỗ khác (CDN, base path riêng) thì đổi bằng configureHevc — xem phần Cấu hình.

2. Bật cross-origin isolation — BẮT BUỘC

Lõi là bản pthread, dùng SharedArrayBuffer, nên trang phải được cách ly:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Thiếu hai header này thì worker chết ngay ở bước cấp bộ nhớ chia sẻ, với lỗi rất mơ hồ. Kiểm tra nhanh trong console: crossOriginIsolated === true.

require-corp chặn mọi tài nguyên cross-origin không khai Cross-Origin-Resource-Policy — kể cả ảnh từ CDN. Ba đường xử lý, theo thứ tự nên thử:

  1. Cross-Origin-Embedder-Policy: credentialless — Chrome 96+ không đòi CORP với tài nguyên no-cors, ảnh cross-origin vẫn tải được.
  2. Đặt header chỉ trên route có player, phần còn lại của app không đụng tới.
  3. Yêu cầu origin của tài nguyên trả Cross-Origin-Resource-Policy: cross-origin.

Next.js:

// next.config.js
async headers() {
  return [{
    source: "/vms/:path*",
    headers: [
      { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
      { key: "Cross-Origin-Embedder-Policy", value: "credentialless" },
    ],
  }];
}

3. Dùng

import { HevcPlayer } from "@oryzateam/hevc-player";

<HevcPlayer url={url} autoPlay muted controls />;

Next.js App Router — component chạy client-only:

const HevcPlayer = dynamic(
  () => import("@oryzateam/hevc-player").then((m) => m.HevcPlayer),
  { ssr: false },
);

Kiểu nguồn tự nhận theo URL; ép bằng sourceType:

| sourceType | Nhận diện tự động khi | | --- | --- | | "moq" | https, cổng 8892, không có đuôi file | | "mp4" | đường dẫn kết thúc .mp4 | | "hls" | còn lại |

Props chính

| Prop | Mặc định | Ý nghĩa | | --- | --- | --- | | url | — | nguồn phát | | sourceType | "auto" | ép kiểu nguồn | | forceEngine | "auto" | "wasm" bỏ qua bước thử <video>; "native" không fallback | | memoryProfile | tự đo | "small" (64MB) hoặc "large" (512MB) — xem bên dưới | | fps | 25 | FPS render mục tiêu | | objectFit | "contain" | như CSS object-fit |

Điều khiển qua ref

const player = useRef<HevcPlayerHandle>(null);

player.current?.play();
player.current?.pause();
player.current?.resume();
player.current?.toggle();
player.current?.stop();
player.current?.seek(30);       // chỉ MP4
await player.current?.unmute(); // chỉ MoQ, phải gọi từ thao tác người dùng

Hook

const player = useHevcPlayer({ url, sourceType: "auto" });
// { status, engine, sourceSize, error, hasAudio, audioMuted,
//   isPlaying, started, posterFrame, videoRef, canvasRef,
//   play, pause, resume, toggle, stop, seek, unmute }

status: idle · loading · queued · playing · paused · ended · error.

queued nghĩa là đang chờ suất giải mã (xem maxConcurrentStreams), khác hẳn loading — UI nên hiện "đang chờ" thay vì spinner tải.

Tiếng

Một API duy nhất cho mọi nhánh: hasAudio, audioMuted, mute(), unmute().

| Nguồn | Tiếng | Bên dưới là gì | | --- | --- | --- | | MoQ | | AudioContext tự dựng — resume()/suspend() | | Native (<video>) | | thuộc tính muted của <video> | | HLS/MP4 qua WASM | không | nhánh này chỉ giải mã video, hasAudio luôn false |

{player.hasAudio && (
  <button onClick={() => (player.audioMuted ? player.unmute() : player.mute())}>
    {player.audioMuted ? "Bật tiếng" : "Tắt tiếng"}
  </button>
)}

unmute() PHẢI gọi từ một thao tác của người dùng — chính sách tự phát chặn cả AudioContext.resume() lẫn việc bỏ muted của <video> đang tự phát. Chiều tắt thì gọi lúc nào cũng được.

Ẩn controls gốc để tự vẽ nút thì vẫn dùng đúng API này, nên nút và tiếng không thể lệch nhau: audioMuted đọc thẳng trạng thái thật, không phải bản sao của lệnh vừa ra.

Với nhánh native, hasAudio dò theo mozHasAudio / audioTracks / webkitAudioDecodedByteCount — không có API chuẩn nào cho việc này. Trên Chrome nó chỉ chuyển sang true sau khi mẩu tiếng đầu tiên được giải mã, tức trễ vài trăm ms so với lúc hình lên.

Vùng nhớ: memoryProfile

Lõi WASM cấp bộ nhớ một lần lúc khởi tạo và không nở ra được (initial === maximum). Vượt trần không phải chậm đi mà là memory access out of bounds rồi worker chết. Nên có hai bản lõi:

| | Vùng nhớ | Luồng pthread | Dùng cho | | --- | --- | --- | --- | | "small" | 64 MB | 1 | ô lưới, nguồn tới 1080p | | "large" | 512 MB | 8 | một luồng, nguồn 4K |

Không truyền thì player tự đo: đọc moov (MP4/HLS) hoặc SPS trong gói khoá đầu tiên (MoQ) rồi chọn. Không tốn request nào — dữ liệu đó vốn đã được parse trong lúc mở nguồn.

Truyền tường minh khi bạn biết rõ hơn nó. Ví dụ lưới 9 ô:

<HevcPlayer memoryProfile="small" … />

Chín ô camera 4K mà để tự đo thì ra chín bản 512 MB — 4.6 GB, tab chết. Ép "small" là nói "chấp nhận không xem 4K ở ô lưới, đổi lại chín ô cùng sống" — quyết định đó thuộc về app, không phải thư viện.

Cấu hình

Gọi một lần lúc bootstrap, trước khi mount player đầu tiên:

import { configureHevc } from "@oryzateam/hevc-player";

configureHevc({ maxConcurrentStreams: 9 });

| Key | Mặc định | Ý nghĩa | | --- | --- | --- | | maxConcurrentStreams | 4 | trần luồng giải mã song song; vượt trần thì xếp hàng (queued) | | targetFps | 25 | FPS render/decode mục tiêu | | maxPendingAccessUnits | 24 | trần backpressure — access unit đã nạp mà worker chưa tiêu thụ | | libavcodecBasePath | /hevc/libavcodec/ | thư mục lõi 512MB | | libavcodecGridBasePath | /hevc/libavcodec-grid/ | thư mục lõi 64MB | | streamWorkerPath | /hevc/stream/hevc-stream-worker.js | worker giải mã | | renderWorkerPath | /hevc/stream/hevc-render-worker.js | worker render | | libavcodecThreads | 8 | pthread cho "large" | | libavcodecGridThreads | 1 | pthread cho "small" | | mp4RangeBytes | 8 MiB | cỡ mỗi HTTP Range khi tải MP4 | | moqFingerprintProxy | — | đổi URL vân tay chứng chỉ MoQ sang proxy cùng origin |

maxConcurrentStreams là van bộ nhớ duy nhất: mỗi luồng giữ một worker libavcodec kèm nguyên vùng heap của nó. Lưới 9 ô nên đặt đúng 9, nếu không sẽ có ô nằm chờ vô hạn — xem trực tiếp thì không ô nào dừng để nhả suất cả.

MoQ (MediaMTX)

Dán thẳng URL trang xem của MediaMTX, kể cả phần credentials:

https://user:pass@host:8892/tên-luồng

Player tự tách thành <luồng>/moq (WebTransport) và <luồng>/fingerprint (vân tay chứng chỉ), giữ credentials để gửi trong MOQT auth token — chúng đi trong bản tin MOQT chứ không phải HTTP basic auth.

Yêu cầu: Chromium, và trang phải ở secure context (WebTransport không tồn tại ngoài https/localhost).

Chứng chỉ của listener QUIC khác chứng chỉ mà cùng cổng đó phục vụ trên TCP — player luôn lấy từ endpoint /fingerprint. Nếu MediaMTX bắt xác thực để đọc endpoint đó thì gọi thẳng từ trình duyệt sẽ chết ở CORS preflight (server không khai Access-Control-Allow-Headers); lúc ấy đi vòng qua server của bạn:

configureHevc({
  moqFingerprintProxy: (url) =>
    `/api/moq/fingerprint?target=${encodeURIComponent(url)}`,
});

Codec: avc3 đi WebCodecs, hev1 đi libavcodec — nên HEVC qua MoQ chạy được cả trên máy không có bộ giải mã HEVC phần cứng.

Playground

npm install
npm run dev        # http://localhost:5183, đã bật sẵn COOP/COEP
npm run dev:media  # sinh nguồn HEVC mẫu bằng ffmpeg

Có sẵn: ô nhập URL, chọn kiểu nguồn (auto/mp4/hls/moq), chọn vùng nhớ, FPS, nút tua, và bảng log realtime kèm badge engine/status/độ phân giải. Lõi lấy thẳng từ assets/hevc/ (Vite dùng làm publicDir) nên không phải chép gì.

Proxy dev bọc URL thành /proxy?url=… để: tách user:pass@ thành header Authorization (trình duyệt chặn URL nhúng credentials), chuyển tiếp Range và header conditional cho nhánh seek, và viết lại URI trong playlist HLS. URL MoQ không đi qua proxy — WebTransport là HTTP/3 trên UDP, proxy TCP không bọc được.

Nút Tua chỉ có tác dụng với MP4 hỗ trợ HTTP Range. Nguồn fMP4 dạng /get?start=… không seek được (sample table rỗng) — player log error, đúng thiết kế: loại này phải xin URL mới từ backend.

Phát triển

npm test        # build rồi chạy test trên dist/
npm run typecheck
npm run build

Test chạy trên dist/ chứ không phải src/: thứ consumer thật sự nhận là bản đã build, nên lỗi ở khâu export/bundle cũng bị bắt.

License

Xem LICENSE. Lõi libavcodec kèm giấy phép riêng trong assets/hevc/libavcodec/LICENSE.