Trong cuộc cách mạng công nghiệp lần thứ tư, Streaming ngày càng phổ biến trong nhiều lĩnh vực như giải trí, bán lẻ và dịch vụ tư vấn. Nếu bạn đang muốn xây dựng một ứng dụng Streaming để hỗ trợ phát triển doanh nghiệp, bạn đã đến đúng nơi. Bài viết này sẽ giúp bạn hiểu rõ về Streaming và cách phát triển ứng dụng Streaming với React Native và GetStream.

Tổng quan về Streaming

Streaming là gì?

Streaming là quá trình truyền liên tục các tệp âm thanh hoặc video từ máy chủ đến thiết bị của người dùng. Nói một cách đơn giản, Streaming là cách người dùng xem TV hoặc nghe Podcast trên các thiết bị có kết nối Internet. Với Streaming, tệp media được phát trên thiết bị người dùng được lưu trữ từ xa và truyền qua Internet theo từng phần nhỏ, chỉ trong vài giây mỗi lần.

Streaming hoạt động như thế nào?

Tương tự như các loại dữ liệu khác được truyền qua Internet, dữ liệu âm thanh và video được chia nhỏ thành các gói dữ liệu (Data Packet). Mỗi gói chứa một phần nhỏ của tệp. Trình phát âm thanh hoặc video trên trình duyệt của thiết bị người dùng sẽ nhận các gói dữ liệu này và xử lý chúng thành nội dung video hoặc âm thanh.

Streaming sử dụng User Datagram Protocol (UDP) hay Transmission Control Protocol (TCP)?

Một số phương thức Streaming sử dụng UDP, trong khi các phương thức khác sử dụng TCP. UDP và TCP là các giao thức truyền tải, được sử dụng để truyền các gói dữ liệu qua mạng. Cả hai đều hoạt động cùng với Internet Protocol (IP).TCP thiết lập một kết nối riêng trước khi truyền dữ liệu và đảm bảo các gói dữ liệu được gửi đến theo đúng thứ tự. Trong khi đó, UDP không thực hiện các cơ chế này. Do đó, TCP có độ tin cậy cao hơn, nhưng truyền dữ liệu qua UDP thường nhanh hơn TCP, mặc dù một số gói dữ liệu có thể bị mất trong quá trình truyền.

Nếu TCP giống như một dịch vụ giao hàng yêu cầu người nhận ký xác nhận khi nhận hàng, thì UDP giống như một dịch vụ giao hàng chỉ cần đặt hàng trước cửa mà không cần gõ cửa để lấy chữ ký. Dịch vụ giao hàng bằng TCP ít làm thất lạc gói hàng hơn, nhưng dịch vụ UDP nhanh hơn vì có thể giao hàng ngay cả khi không có ai ở nhà để ký nhận.
Đối với Streaming, trong một số trường hợp, tốc độ quan trọng hơn nhiều so với độ tin cậy. Ví dụ, khi tham gia một cuộc họp trực tuyến, người dùng sẽ ưu tiên tương tác với những người tham dự khác theo thời gian thực thay vì phải chờ mọi dữ liệu được truyền đầy đủ. Vì vậy, việc mất một vài gói dữ liệu không phải là vấn đề quá nghiêm trọng và UDP nên được sử dụng.
Trong một số trường hợp khác, độ tin cậy lại quan trọng hơn đối với Streaming. Ví dụ, HTTP Live Streaming (HLS) và MPEG-DASH đều là các giao thức Streaming sử dụng TCP để truyền tải dữ liệu. Nhiều dịch vụ Video theo yêu cầu (Video-on-Demand) cũng sử dụng TCP.

Buffering là gì?

Các trình phát Streaming thường tải trước một vài giây nội dung để video hoặc âm thanh vẫn có thể tiếp tục phát nếu kết nối bị gián đoạn trong thời gian ngắn. Quá trình này được gọi là Buffering. Buffering giúp video phát mượt mà và liên tục. Tuy nhiên, với kết nối quá chậm hoặc mạng có độ trễ cao, video có thể mất nhiều thời gian để Buffering.

Xây dựng ứng dụng Streaming với React Native sử dụng GetStream

Trong hướng dẫn này, chúng ta sẽ bắt đầu trực tiếp với việc xây dựng ứng dụng Streaming mà không đi sâu vào các bước thiết lập React Native bổ sung.

Để xây dựng một hệ thống Streaming hoàn chỉnh, thông thường chúng ta cần phát triển Backend, CMS và ứng dụng di động. Tuy nhiên, trong hướng dẫn này, chúng ta sẽ tập trung vào việc xây dựng ứng dụng Streaming chỉ với React Native. Để thực hiện, chúng ta sẽ sử dụng thư viện bên thứ ba có tên GetStream. GetStream.

Để cài đặt Stream Video React Native SDK, hãy chạy lệnh sau trong Terminal mà bạn sử dụng:

yarn add @stream-io/video-react-native-sdk
Stream Video React Native SDK yêu cầu cài đặt một số peer dependencies để cung cấp trải nghiệm gọi video tốt nhất. Bạn có thể chạy lệnh sau để cài đặt các dependency này:
yarn add @stream-io/react-native-webrtc \\
   react-native-incall-manager react-native-svg \\
   @react-native-community/netinfo
npx pod-install

Cài đặt dành riêng cho Android

Trong android/build.gradle, thêm đoạn sau vào phần buildscript:
buildscript {
    ext {
        ...
        minSdkVersion = 24
    }
    ...
}

Bật hỗ trợ Java 8
Trong android/app/build.gradle, thêm đoạn sau vào phần android:

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_11
}

Tùy chọn: Hỗ trợ R8/ProGuard
Nếu cần hỗ trợ R8/ProGuard, trong file android/app/proguard-rules.pro, thêm dòng sau:

-keep class org.webrtc.** { *; }

Khai báo quyền

Để thực hiện cuộc gọi video hoặc âm thanh, ứng dụng cần sử dụng camera và microphone của thiết bị. Do đó, cần khai báo các quyền tương ứng trên cả hai nền tảng.

iOS

Tối thiểu, thêm các key và value sau vào file Info.plist:
Privacy – Camera Usage Description: “ requires camera access to capture and transmit video”- Privacy – Microphone Usage Description: “ requires microphone access to capture and transmit audio”

Android

Trong AndroidManifest.xml add the following permissions before the <application> section.

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.audio.output" />
<uses-feature android:name="android.hardware.microphone" />

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" />
Nếu bạn dự định hỗ trợ thêm thiết bị Bluetooth, hãy thêm đoạn lệnh sau:
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

Tiếp theo, chúng ta sẽ xây dựng hai màn hình: màn hình Host và màn hình Viewer.

StreamingHostScreen.tsx
import React, { useEffect, useLayoutEffect } from "react";
import {
  HostLivestream,
  StreamCall,
  StreamVideo,
  StreamVideoClient,
  User,
} from "@stream-io/video-react-native-sdk";
import { AppStackScreenProp } from "navigators";
import { Layout } from "components";
import { HostLivestreamControlsView } from "./HostLivestreamControlsView";
import { HostLivestreamTopView } from "./HostLivestreamTopView";

import IncallManager from "react-native-incall-manager";

const apiKey = "mmhfdzb5evj2"; // API key for Stream Video SDK
const token =
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3Byb250by5nZXRzdHJlYW0uaW8iLCJzdWIiOiJ1c2VyL0dlbmVyYWxfR3JpZXZvdXMiLCJ1c2VyX2lkIjoiR2VuZXJhbF9Hcmlldm91cyIsInZhbGlkaXR5X2luX3NlY29uZHMiOjYwNDgwMCwiaWF0IjoxNzI2MDQ3MzYyLCJleHAiOjE3MjY2NTIxNjJ9.GDMFP4wf3DifOw1ZmxO0KSy2HG_t4ByFv2kKwW5hVp4"; // JWT token for user authentication
const userId = "General_Grievous"; // User ID for the stream
const callId = "FMeNGvwQJY26"; // Call ID for the livestream session

// StreamingHostScreen functional component
export const StreamingHostScreen: React.FC<
  AppStackScreenProp<"StreamingHost">
> = ({ navigation }) => {
  const user: User = { id: userId, name: "SparkMinds Host" }; // User details

  // Get or create an instance of the StreamVideoClient
  const client = StreamVideoClient.getOrCreateInstance({ apiKey, user, token });

  // Initialize a call of type 'livestream' with the given callId
  const call = client.call("livestream", callId);

  // useEffect to handle joining and ending the livestream
  useEffect(() => {
    call.join({ create: true }); // Join or create the livestream
    IncallManager.start({ media: "video" }); // Start the call with video

    return () => {
      call.endCall(); // Clean up by ending the call
      IncallManager.stop(); // Stop the call manager when component unmounts
    };
  }, []);

  // useLayoutEffect to hide the default header
  useLayoutEffect(() => {
    navigation.setOptions({
      headerShown: false, // Hide the navigation header
    });
  }, []);

  // Main JSX layout for hosting the livestream
  return (
    <Layout>
      {/* StreamVideo provider for handling video stream */}
      <StreamVideo client={client}>
        {/* StreamCall component wrapping the livestream */}
        <StreamCall call={call}>
          {/* HostLivestream component, providing custom top view and controls */}
          <HostLivestream
            HostLivestreamTopView={HostLivestreamTopView} // Top view for the host stream UI
            HostLivestreamControls={HostLivestreamControlsView} // Controls for managing the stream
          />
        </StreamCall>
      </StreamVideo>
    </Layout>
  );
};

HostLivestreamTopView.tsx

import React from "react";
import {
  DurationBadge,
  FollowerCount,
  HostLivestreamTopViewProps,
  LiveIndicator,
} from "@stream-io/video-react-native-sdk";
import { SViewStyle } from "models";
import { View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { style } from "theme";

export const HostLivestreamTopView: React.FC<
  HostLivestreamTopViewProps
> = () => {
  const insets = useSafeAreaInsets(); // Get the safe area insets to handle devices with notches

  return (
    // Main container for the top view, with top padding based on safe area insets
    <View style={[$topView, { paddingTop: insets.top }]}>
      {/* Displays the stream duration for the host */}
      <DurationBadge mode="host" />

      {/* Spacer view to push other elements apart */}
      <View style={style.flex_1} />

      {/* Live indicator showing the stream is currently live */}
      <LiveIndicator />

      {/* Displays the number of followers watching the stream */}
      <FollowerCount />
    </View>
  );
};

// Styling for the top view container, arranging elements in a row with padding
const $topView: SViewStyle = [style.row, style.px_md];

HostLivestreamControlsView.tsx

import React from "react";
import {
  HostLivestreamControlsProps,
  HostStartStreamButton,
  LivestreamMediaControls,
} from "@stream-io/video-react-native-sdk";
import { SViewStyle } from "models";
import { View } from "react-native";
import { spacing, style } from "theme";
import { useSafeAreaInsets } from "react-native-safe-area-context";

type HostLivestreamControlsViewProps = {} & HostLivestreamControlsProps;

// Main functional component for controlling the host livestream
export const HostLivestreamControlsView: React.FC<
  HostLivestreamControlsViewProps
> = ({
  onEndStreamHandler, // Handler for ending the stream
  onStartStreamHandler, // Handler for starting the stream
  hls, // HLS (HTTP Live Streaming) protocol flag or configuration
  disableStopPublishedStreamsOnEndStream, // Option to disable stopping published streams when ending
  onLayout, // Layout handler, typically for UI measurement purposes
}) => {
  const insets = useSafeAreaInsets(); // Get safe area insets (e.g., for iPhone with notches)

  return (
    // Root view containing controls with safe area padding and layout handling
    <View
      style={[$root, { paddingBottom: insets.bottom || spacing.md }]} // Adding bottom padding based on safe area insets
      onLayout={onLayout} // Handling layout changes
    >
      {/* Start/End stream button, passing through event handlers and stream config */}
      <HostStartStreamButton
        onEndStreamHandler={onEndStreamHandler}
        onStartStreamHandler={onStartStreamHandler}
        hls={hls}
        disableStopPublishedStreamsOnEndStream={
          disableStopPublishedStreamsOnEndStream
        }
      />
      {/* Media controls for interacting with the livestream (play, pause, etc.) */}
      <LivestreamMediaControls />
    </View>
  );
};

// Styles for the root view using project-defined styles
const $root: SViewStyle = [style.row_between, style.px_md, style.pt_md];

StreamingViewerScreen.tsx

import React, { useLayoutEffect } from "react";
import { Layout } from "components";
import { AppStackScreenProp } from "navigators";
import {
  LivestreamPlayer,
  StreamVideo,
  StreamVideoClient,
  User,
} from "@stream-io/video-react-native-sdk";
import { ViewerLivestreamView } from "./ViewerLivestreamView";

const apiKey = "mmhfdzb5evj2"; // API key for Stream Video SDK
const token =
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3Byb250by5nZXRzdHJlYW0uaW8iLCJzdWIiOiJ1c2VyL0dlbmVyYWxfR3JpZXZvdXMiLCJ1c2VyX2lkIjoiR2VuZXJhbF9Hcmlldm91cyIsInZhbGlkaXR5X2luX3NlY29uZHMiOjYwNDgwMCwiaWF0IjoxNzI2MDQ3MzYyLCJleHAiOjE3MjY2NTIxNjJ9.GDMFP4wf3DifOw1ZmxO0KSy2HG_t4ByFv2kKwW5hVp4"; // JWT token for user authentication
const userId = "General_Grievous"; // User ID for the viewer
const callId = "FMeNGvwQJY26"; // Call ID for the livestream session

const user: User = { id: userId, name: "SparkMinds Viewer" }; // Viewer user details

// StreamingViewerScreen functional component
export const StreamingViewerScreen: React.FC<
  AppStackScreenProp<"StreamingViewer">
> = ({ navigation }) => {
  // Create an instance of the StreamVideoClient with the given API key, user details, and token
  const client = StreamVideoClient.getOrCreateInstance({ apiKey, user, token });

  // useLayoutEffect to set up the navigation header
  useLayoutEffect(() => {
    navigation.setOptions({
      headerShown: false, // Hide the navigation header while viewing the livestream
    });
  }, []);

  // Main JSX layout for the livestream viewer
  return (
    <Layout>
      {/* StreamVideo provider to handle video stream for the viewer */}
      <StreamVideo client={client}>
        {/* LivestreamPlayer to manage the livestream playback */}
        <LivestreamPlayer
          callType="livestream" // Specifies the type of stream (livestream)
          callId={callId} // Passes the callId for the specific livestream session
          ViewerLivestream={ViewerLivestreamView} // Custom viewer view for the livestream UI
        />
      </StreamVideo>
    </Layout>
  );
};
ViewerLivestreamView.tsx
import React, { useEffect } from "react";
import { StyleSheet } from "react-native";
import InCallManager from "react-native-incall-manager";
import { useCallStateHooks } from "@stream-io/video-react-bindings";
import { hasVideo } from "@stream-io/video-client";
import {
  DurationBadge,
  FloatingParticipantView,
  FollowerCount,
  LiveIndicator,
  LivestreamLayout,
  useTheme,
  ViewerLivestreamProps,
  ViewerLivestreamTopViewProps,
} from "@stream-io/video-react-native-sdk";
import { Layout } from "components";
import { ViewerLivestreamTopView } from "./ViewerLivestreamTopView";
import { ViewerLivestreamControls } from "./ViewerLivestreamControls";

type ViewerLivestreamViewProps = {} & ViewerLivestreamProps;

export const ViewerLivestreamView: React.FC<ViewerLivestreamViewProps> = ({
  onLeaveStreamHandler,
}) => {
  const {
    theme: { viewerLivestream }, // Get the theme for viewer livestream
  } = useTheme();
  const { useHasOngoingScreenShare, useParticipants } = useCallStateHooks(); // Hooks for call state management
  const hasOngoingScreenShare = useHasOngoingScreenShare(); // Check if there is an ongoing screen share
  const [currentSpeaker] = useParticipants(); // Get the current speaker from participants
  const floatingParticipant =
    hasOngoingScreenShare &&
    currentSpeaker &&
    hasVideo(currentSpeaker) && // Check if the current speaker has video
    currentSpeaker;

  const [topViewHeight, setTopViewHeight] = React.useState<number>(); // State for the height of the top view
  const [controlsHeight, setControlsHeight] = React.useState<number>(); // State for the height of the controls

  // Automatically route audio to speaker devices as relevant for watching videos
  useEffect(() => {
    InCallManager.start({ media: "video" }); // Start the in-call manager for video
    return () => InCallManager.stop(); // Stop the in-call manager when the component unmounts
  }, []);

  // Props for the top view of the livestream
  const topViewProps: ViewerLivestreamTopViewProps = {
    LiveIndicator,
    FollowerCount,
    DurationBadge,
    onLayout: (event) => {
      setTopViewHeight(event.nativeEvent.layout.height); // Set the height of the top view
    },
  };

  return (
    <Layout style={viewerLivestream.container}>
      {" "}
      {/* Main layout for the viewer livestream */}
      <ViewerLivestreamTopView {...topViewProps} />{" "}
      {/* Render the top view with props */}
      {floatingParticipant && ( // Render floating participant if available
        <FloatingParticipantView
          participant={floatingParticipant} // Pass the floating participant
          draggableContainerStyle={[
            StyleSheet.absoluteFill, // Fill the container absolutely
            {
              top: topViewHeight, // Position based on top view height
              bottom: controlsHeight, // Position based on controls height
            },
          ]}
        />
      )}
      <LivestreamLayout /> {/* Render the main livestream layout */}
      <ViewerLivestreamControls
        onLeaveStreamHandler={onLeaveStreamHandler} // Pass handler for leaving the stream
        onLayout={(event) => {
          setControlsHeight(event.nativeEvent.layout.height); // Set the height of the controls
        }}
      />
    </Layout>
  );
};
ViewerLivestreamTopView.tsx
import React from "react";
import {
  FollowerCount,
  LiveIndicator,
  ViewerLivestreamTopViewProps,
} from "@stream-io/video-react-native-sdk";
import { SViewStyle } from "models";
import { View } from "react-native";
import { style } from "theme";
import { useSafeAreaInsets } from "react-native-safe-area-context";

export const ViewerLivestreamTopView: React.FC<
  ViewerLivestreamTopViewProps
> = ({ onLayout }) => {
  const insets = useSafeAreaInsets(); // Get safe area insets for proper padding

  return (
    <View onLayout={onLayout} style={[$root, { paddingTop: insets.top }]}>
      {/* Indicator showing the stream is live */}
      <LiveIndicator />
      {/* Displays the number of followers watching the stream */}
      <FollowerCount />
    </View>
  );
};

const $root: SViewStyle = [
  style.row,
  style.abs,
  style.px_md,
  { left: 0, top: 0, zIndex: 1 },
  style.w_screenWidth,
];
ViewerLivestreamControls.tsx
import {
  ViewerLeaveStreamButton,
  ViewerLivestreamControlsProps,
} from "@stream-io/video-react-native-sdk";
import { SViewStyle } from "models";
import React from "react";
import { View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { spacing, style } from "theme";

export const ViewerLivestreamControls: React.FC<
  ViewerLivestreamControlsProps
> = ({ onLeaveStreamHandler, onLayout }) => {
  const insets = useSafeAreaInsets(); // Get safe area insets for bottom padding

  return (
    <View
      onLayout={onLayout} // Handle layout changes
      style={[$root, { paddingBottom: insets.bottom || spacing.md }]} // Apply styles and padding for safe area
    >
      {/* Button for viewers to leave the stream */}
      <ViewerLeaveStreamButton onLeaveStreamHandler={onLeaveStreamHandler} />
    </View>
  );
};

const $root: SViewStyle = [
  style.abs,
  style.bottom_0,
  style.left_0,
  style.right_0,
  style.row,
  style.px_md,
  style.py_lg,
];

Kết luận Bằng cách làm theo hướng dẫn này, bạn có thể xây dựng một ứng dụng Streaming cơ bản với React Native. Nếu muốn phát triển một ứng dụng Streaming toàn diện và chuyên nghiệp hơn, bạn có thể lựa chọn hợp tác với một đơn vị gia công phần mềm (Software Outsourcing) như SparkMinds để được tư vấn và hỗ trợ triển khai giải pháp phù hợp. hãy liên hệHãy chia sẻ thông tin chi tiết về dự án và nhận báo giá từ chúng tôi. 
Chúng tôi luôn sẵn sàng ký kết một NDA để bảo mật thông tin của bạn!