> ## Documentation Index
> Fetch the complete documentation index at: https://docs.livepeer.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Go Livepeer Changelog

> Changelog of updates, improvements, and changes related to the Go Livepeer implementation

export const CustomCardTitle = ({icon, title, variant = "card", iconSize, style = {}, className = "", ...rest}) => {
  const variants = {
    card: {
      display: 'flex',
      alignItems: 'center',
      gap: "var(--lp-spacing-2)",
      marginBottom: "var(--lp-spacing-3)",
      color: 'var(--lp-color-text-primary)',
      fontSize: '1rem',
      fontWeight: 600
    },
    accordion: {
      display: 'inline-flex',
      alignItems: 'center',
      gap: "var(--lp-spacing-2)"
    },
    tab: {
      display: 'inline-flex',
      alignItems: 'center',
      gap: '0.4rem',
      fontSize: '0.875rem'
    }
  };
  const sizes = {
    card: 20,
    accordion: 18,
    tab: 14
  };
  const size = iconSize || sizes[variant] || 20;
  const baseStyle = variants[variant] || variants.card;
  return variant === 'card' ? <div className={className} style={{
    ...baseStyle,
    ...style
  }} {...rest}>
      {typeof icon === 'string' ? <Icon icon={icon} size={size} color="var(--lp-color-accent)" /> : icon}
      {title}
    </div> : <span className={className} style={{
    ...baseStyle,
    ...style
  }} {...rest}>
      {typeof icon === 'string' ? <Icon icon={icon} size={size} color="var(--lp-color-accent)" /> : icon}
      {title}
    </span>;
};

export const Subtitle = ({style = {}, text, children, variant = 'default', fontSize = '', fontWeight = '', fontStyle = '', marginTop = '', marginBottom = '', color = '', className = '', ...rest}) => {
  const renderInlineCode = (value, keyPrefix) => {
    return value.split(/(`[^`]+`)/g).map((segment, index) => {
      if (segment.startsWith('`') && segment.endsWith('`')) {
        return <code key={`${keyPrefix}-code-${index}`}>{segment.slice(1, -1)}</code>;
      }
      return segment;
    });
  };
  const renderInlineMarkup = (value, keyPrefix = 'subtitle') => {
    if (typeof value !== 'string') {
      return value;
    }
    return value.split(/(\*\*[\s\S]+?\*\*)/g).map((segment, index) => {
      if (segment.startsWith('**') && segment.endsWith('**')) {
        const inner = segment.slice(2, -2);
        return <strong key={`${keyPrefix}-strong-${index}`}>
            {renderInlineCode(inner, `${keyPrefix}-strong-${index}`)}
          </strong>;
      }
      return renderInlineCode(segment, `${keyPrefix}-${index}`);
    });
  };
  const renderContent = (value, keyPrefix) => {
    if (Array.isArray(value)) {
      return value.map((item, index) => renderContent(item, `${keyPrefix}-${index}`));
    }
    return renderInlineMarkup(value, keyPrefix);
  };
  const variants = {
    default: {
      fontSize: '1rem',
      fontStyle: 'italic',
      color: 'var(--lp-color-accent)',
      marginBottom: 0
    },
    changelog: {
      fontSize: '0.8rem',
      fontStyle: 'normal',
      fontWeight: 700,
      color: 'var(--lp-color-text-primary)',
      marginBottom: 0
    }
  };
  const base = variants[variant] || variants.default;
  return <span className={className} style={{
    ...base,
    ...fontSize ? {
      fontSize
    } : {},
    ...fontWeight ? {
      fontWeight
    } : {},
    ...fontStyle ? {
      fontStyle
    } : {},
    ...marginTop ? {
      marginTop
    } : {},
    ...marginBottom ? {
      marginBottom
    } : {},
    ...color ? {
      color
    } : {},
    ...style
  }} {...rest}>
      {renderContent(text, 'text')}
      {renderContent(children, 'children')}
    </span>;
};

export const LazyLoad = ({children, height = "200px", offset = "200px", fadeDuration = 400, className = "", style = {}, ...rest}) => {
  const ref = useRef(null);
  const [visible, setVisible] = useState(false);
  const [ready, setReady] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setVisible(true);
        observer.disconnect();
      }
    }, {
      rootMargin: offset
    });
    observer.observe(el);
    return () => observer.disconnect();
  }, []);
  useEffect(() => {
    if (!visible) return;
    const frameId = requestAnimationFrame(() => {
      setReady(true);
    });
    return () => cancelAnimationFrame(frameId);
  }, [visible]);
  const placeholder = <div ref={ref} className={className} style={{
    minHeight: height,
    ...style
  }} {...rest} />;
  if (!visible) return placeholder;
  return <div ref={ref} className={className} style={{
    opacity: ready ? 1 : 0,
    transition: `opacity ${fadeDuration}ms ease-in`,
    ...style
  }} {...rest}>
      {children}
    </div>;
};

export const ScrollBox = ({children, maxHeight = 300, showHint = true, ariaLabel = "Scrollable content", style = {}, className = "", ...rest}) => {
  const contentRef = useRef(null);
  const [isOverflowing, setIsOverflowing] = useState(false);
  useEffect(() => {
    const checkOverflow = () => {
      if (contentRef.current) {
        const maxHeightPx = typeof maxHeight === "number" ? maxHeight : parseInt(maxHeight, 10) || 300;
        setIsOverflowing(contentRef.current.scrollHeight > maxHeightPx);
      }
    };
    checkOverflow();
    window.addEventListener("resize", checkOverflow);
    return () => window.removeEventListener("resize", checkOverflow);
  }, [maxHeight, children]);
  return <div className={className} style={{
    position: "relative",
    ...style
  }} {...rest}>
      <div ref={contentRef} role="region" tabIndex={0} aria-label={ariaLabel} style={{
    maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
    overflowY: "auto",
    paddingRight: 4
  }} onScroll={e => {
    const el = e.target;
    const atBottom = el.scrollHeight - el.scrollTop <= el.clientHeight + 10;
    const hint = el.parentNode.querySelector("[data-scroll-hint]");
    if (hint) hint.style.opacity = atBottom ? "0" : "1";
  }}>
        {children}
      </div>
      {showHint && isOverflowing && <div data-scroll-hint style={{
    fontSize: 11,
    color: "var(--lp-color-text-muted)",
    textAlign: "center",
    marginTop: 8,
    transition: "opacity 0.2s"
  }}>
          Scroll for more ↓
        </div>}
    </div>;
};

export const DoubleIconLink = ({label = '', labelColor, href = '#', text = '', iconLeft = 'github', iconLeftColor, iconRight = 'arrow-up-right', iconRightColor = 'var(--lp-color-accent)', className = '', style = {}, ...rest}) => {
  return <span className={className} style={{
    whiteSpace: 'nowrap',
    display: 'inline-flex',
    alignItems: 'center',
    gap: "var(--lp-spacing-1)",
    marginLeft: '0.3rem',
    ...style
  }} {...rest}>
      {text && <span style={{
    marginRight: 8
  }}>{text}</span>}
      <Icon icon={iconLeft} color={iconLeftColor} />
      <a href={href} style={{
    color: {
      labelColor
    }
  }}>
        {label}
      </a>
      <div style={{
    marginRight: '0.3rem'
  }}>
        <Icon icon={iconRight} size={12} color={iconRightColor} />
      </div>
    </span>;
};

export const LinkArrow = ({href, label, description, newline = true, borderColor, className = '', style = {}, ...rest}) => {
  const linkArrowStyle = {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: "var(--lp-spacing-1)",
    width: 'fit-content',
    ...borderColor && ({
      borderColor
    })
  };
  return <span className={className} style={style} {...rest}>
      {newline && <br />}
      <span style={linkArrowStyle}>
        <a href={href} target="_blank" rel="noopener noreferrer">
          {label}
        </a>
        <Icon icon="arrow-up-right" size={14} color="var(--lp-color-accent)" />
      </span>
      {description && description}
      {description && <div style={{
    height: "var(--lp-spacing-3)"
  }} />}
    </span>;
};

export const InlineDivider = ({margin = "0.75rem 0", padding = "0", color = "var(--lp-color-border-default)", opacity = 0.4, height = "1px", className = "", style = {}, ...rest}) => <hr role="separator" className={className} style={{
  border: "none",
  margin,
  padding,
  height,
  backgroundColor: color,
  opacity,
  ...style
}} {...rest} />;

export const CustomDivider = ({color = "var(--lp-color-border-default)", middleText = "", spacing = "default", style = {}, className = "", ...rest}) => {
  const spacingPresets = {
    default: {
      margin: "24px 0"
    },
    overlap: {
      margin: "-1rem 0 -1rem 0"
    },
    tight: {
      margin: "0 0 -1rem 0"
    },
    section: {
      margin: "0 0 -2rem 0"
    },
    sectionOverlap: {
      margin: "-1rem 0 -2rem 0"
    },
    deepOverlap: {
      margin: "-1rem 0 -1.5rem 0"
    }
  };
  const spacingStyle = spacingPresets[spacing] || spacingPresets.default;
  return <div role="separator" aria-orientation="horizontal" className={className} style={{
    display: "flex",
    alignItems: "center",
    ...spacingStyle,
    fontSize: style?.fontSize || "16px",
    height: "fit-content",
    ...style
  }} {...rest}>
      <span style={{
    marginRight: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
      </span>
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      {middleText && <>
          <Icon icon="circle" size={2} />
          <span style={{
    margin: "0 8px",
    fontWeight: "bold",
    color: color,
    opacity: 0.7
  }}>
            {middleText}
          </span>
          <Icon icon="circle" size={2} />
        </>}
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      <span style={{
    marginLeft: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <span style={{
    display: "inline-block",
    transform: "scaleX(-1)"
  }}>
          <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
        </span>
      </span>
    </div>;
};

<Tip>
  This page is an automated workflow.
  <Subtitle variant="changelog" style={{fontSize: "0.95rem", marginTop: "0.25rem"}}>Subscribe to this changelog's <LinkArrow label="RSS Feed" href="/v2/resources/changelog/protocol/go-livepeer/rss.xml" newline={false} /></Subtitle>
</Tip>

<CustomDivider style={{margin: "-0.5rem 0 -1.5rem 0"}} />

Release notes for the Livepeer node software. For protocol-level changes, see the [Livepeer Improvement Proposals (LIPs)](https://github.com/livepeer/LIPs). For full release binaries, see the [GitHub releases page](https://github.com/livepeer/go-livepeer/releases).

<CustomDivider />

<Update label="v0.8.10" tags={["Live AI", "BYOC", "Bug Fixes"]} rss={{ title: "go-livepeer v0.8.10", description: "Remote signer support for Live AI gateways, unified BYOC capability discovery, and broadcaster crash fix" }} description={<Subtitle variant="changelog">March 2026</Subtitle>}>
  ## v0.8.10

  ### New features

  * **Remote signer for Live AI** - Gateways running Live AI (live-video-to-video) workloads can now delegate Ethereum payment signing to a separate remote signer service. This isolates private keys from the gateway process and enables keyless gateway deployments. The remote signer also supports orchestrator discovery, so lightweight gateways can rely on the signer node for both signing and network discovery. See the [remote signers guide](/v2/gateways/guides/payments-and-pricing/remote-signers) for setup details.

  ### Updates

  * **Unified BYOC capability discovery** - BYOC (Bring Your Own Compute) capabilities and pricing are now included in the standard orchestrator capability response. Clients no longer need a separate call to discover which BYOC pipelines an orchestrator offers. This makes BYOC orchestrators visible through the same [capability system](/v2/gateways/concepts/capabilities) used for built-in workloads.
  * **BYOC orchestrator swap reporting** - BYOC streaming sessions now report orchestrator swaps using the same event format as Live AI sessions, improving observability for operators.
  * **LPMS update** - The media processing library (LPMS) has been updated with fixes for out-of-order timestamps and runaway encodes, improving transcoding stability.

  ### Bug fixes

  * **Fixed crash in broadcaster max price calculation** - Resolved a crash that occurred when no per-capability price constraints were set. The broadcaster now correctly falls back to the global max price.
  * **Fixed BYOC orchestrator streaming reserve capacity** - Resolved a bug where BYOC orchestrators were not properly tracking reserved streaming capacity, which could cause incorrect orchestrator selection.
</Update>

<Update label="v0.8.9" tags={["BYOC", "AI Models", "Bug Fixes"]} rss={{ title: "go-livepeer v0.8.9", description: "BYOC streaming workloads, new AI model support, WHEP playback improvements, and stability fixes" }} description={<Subtitle variant="changelog">January 2026</Subtitle>}>
  ## v0.8.9

  ### New features

  * **BYOC streaming** - Orchestrators can now process streaming workloads through the BYOC pipeline, enabling real-time compute jobs alongside existing batch capabilities. See the [BYOC tutorial](/v2/gateways/guides/tutorials/tutorial-2-byoc-cpu-pipeline) for a walkthrough.
  * **New AI model support** - Added support for SD 1.5 and SDXL video-to-video pipelines, along with Scope pipeline support for orchestrator workers. These expand the range of [AI pipelines](/v2/developers/concepts/ai-on-livepeer) available on the network.
  * **Custom AI models JSON for Docker** - Operators running the Docker-based Livepeer Box can now specify a custom AI models configuration file, making it easier to tailor model availability.

  ### Updates

  * **WHEP negotiation improvements** - The WebRTC playback server (WHEP) now handles MPEG-TS initialisation errors as 404 responses and limits concurrent sessions, improving reliability for Live AI playback.
  * **Improved latency reporting** - Gateway-to-orchestrator latency measurement has been refined for more accurate performance tracking.

  ### Bug fixes

  * **Fixed crash during RTMP ingest cleanup** - Resolved a panic that could occur when cleaning up RTMP ingest sessions for Live AI.
  * **Fixed crash when monitoring is disabled** - The node no longer panics if the monitoring subsystem is not enabled.
  * **Fixed concurrent map write panic** - Resolved a race condition that could cause a crash during concurrent Live AI session management.
  * **Fixed orchestrator stream setup failure handling** - BYOC orchestrators now handle stream setup failures gracefully instead of leaving sessions in a broken state.
  * **Fixed orphaned containers on shutdown** - Worker containers are now properly cleaned up when the node shuts down, preventing resource leaks.
</Update>

<CustomDivider />

## Related resources

<CardGroup cols={2}>
  <Card title="GitHub releases" icon="github" href="https://github.com/livepeer/go-livepeer/releases" arrow>
    Download binaries and view full release notes
  </Card>

  <Card title="Remote signers" icon="key" href="/v2/gateways/guides/payments-and-pricing/remote-signers" arrow>
    Set up keyless gateway operation with remote signing
  </Card>

  <Card title="Gateway capabilities" icon="grid-2" href="/v2/gateways/concepts/capabilities" arrow>
    How gateways discover and route to orchestrator capabilities
  </Card>

  <Card title="AI on Livepeer" icon="microchip-ai" href="/v2/developers/concepts/ai-on-livepeer" arrow>
    Overview of AI pipeline categories on the network
  </Card>
</CardGroup>
