Project One-Click: Need help with cc4c

Here's my main.js where the Peacock code works as it should and unmutes: (you'll see it near the bottom)

const {launch: puppeteerLaunch} = require('puppeteer-core')
const {launch, getStream} = require('puppeteer-stream')
const fs = require('fs')
const child_process = require('child_process')
const process = require('process')
const path = require('path')
const express = require('express')
const morgan = require('morgan')
require('express-async-errors')
require('console-stamp')(console, {
  format: ':date(yyyy/mm/dd HH:MM:ss.l)',
})

const viewport = {
  width: 1920,
  height: 1080,
}

var currentBrowser, dataDir, lastPage
const getCurrentBrowser = async () => {
  if (!currentBrowser || !currentBrowser.isConnected()) {
    currentBrowser = await launch(
      {
        launch: opts => {
          if (process.pkg) {
            opts.args = opts.args.filter(
              arg => !arg.startsWith('--load-extension=') && !arg.startsWith('--disable-extensions-except=')
            )
            opts.args = opts.args.concat([
              `--load-extension=${path.join(dataDir, 'extension')}`,
              `--disable-extensions-except=${path.join(dataDir, 'extension')}`,
            ])
          }
          if (process.env.DOCKER || process.platform == 'win32') {
            opts.args = opts.args.concat(['--no-sandbox'])
          }
          return puppeteerLaunch(opts)
        },
      },
      {
        executablePath: getExecutablePath(),
        defaultViewport: null, // no viewport emulation
        userDataDir: path.join(dataDir, 'chromedata'),
        args: [
          '--no-first-run',
          '--disable-infobars',
          '--hide-crash-restore-bubble',
          '--disable-blink-features=AutomationControlled',
          '--hide-scrollbars',
        ],
        ignoreDefaultArgs: [
          '--enable-automation',
          '--disable-extensions',
          '--disable-default-apps',
          '--disable-component-update',
          '--disable-component-extensions-with-background-pages',
          '--enable-blink-features=IdleDetection',
        ],
      }
    )
    currentBrowser.on('close', () => {
      currentBrowser = null
    })
    currentBrowser.pages().then(pages => {
      pages.forEach(page => page.close())
    })
  }
  return currentBrowser
}

const getExecutablePath = () => {
  if (process.env.CHROME_BIN) {
    return process.env.CHROME_BIN
  }

  let executablePath
  if (process.platform === 'linux') {
    try {
      executablePath = child_process.execSync('which chromium-browser').toString().split('\n').shift()
    } catch (e) {
      // NOOP
    }

    if (!executablePath) {
      executablePath = child_process.execSync('which chromium').toString().split('\n').shift()
      if (!executablePath) {
        throw new Error('Chromium not found (which chromium)')
      }
    }
  } else if (process.platform === 'darwin') {
    executablePath = [
      '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
      '/Applications/Chromium.app/Contents/MacOS/Chromium',
      '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
    ].find(fs.existsSync)
  } else if (process.platform === 'win32') {
    executablePath = [
      `C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe`,
      `C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe`,
      path.join(process.env.USERPROFILE, 'AppData', 'Local', 'Google', 'Chrome', 'Application', 'chrome.exe'),
      path.join(process.env.USERPROFILE, 'AppData', 'Local', 'Chromium', 'Application', 'chrome.exe'),
    ].find(fs.existsSync)
  } else {
    throw new Error('Unsupported platform: ' + process.platform)
  }

  return executablePath
}

async function main() {
  dataDir = process.cwd()
  if (process.pkg) {
    switch (process.platform) {
      case 'darwin':
        dataDir = path.join(process.env.HOME, 'Library', 'Application Support', 'ChromeCapture')
        break
      case 'win32':
        dataDir = path.join(process.env.USERPROFILE, 'AppData', 'Local', 'ChromeCapture')
        break
    }
    let out = path.join(dataDir, 'extension')
    fs.mkdirSync(out, {recursive: true})
    ;['manifest.json', 'background.js', 'options.html', 'options.js'].forEach(file => {
      fs.copyFileSync(
        path.join(process.pkg.entrypoint, '..', 'node_modules', 'puppeteer-stream', 'extension', file),
        path.join(out, file)
      )
    })
  }

  const app = express()

  const df = require('dateformat')
  morgan.token('mydate', function (req) {
    return df(new Date(), 'yyyy/mm/dd HH:MM:ss.l')
  })
  app.use(morgan('[:mydate] :method :url from :remote-addr responded :status in :response-time ms'))

  app.get('/', (req, res) => {
    res.send(
      `<html>
  <title>Chrome Capture for Channels</title>
  <h2>Chrome Capture for Channels</h2>
  <p>Usage: <code>/stream?url=URL</code> or <code>/stream/&lt;name></code></p>
  <pre>
  #EXTM3U

  #EXTINF:-1 channel-id="windy",Windy
  chrome://${req.get('host')}/stream/windy

  #EXTINF:-1 channel-id="weatherscan",Weatherscan
  chrome://${req.get('host')}/stream/weatherscan
  </pre>
  </html>`
    )
  })

  app.get('/debug', async (req, res) => {
    res.send(`<html>
    <script>
    async function videoClick(e) {
      e.target.focus()
      let x = ((e.clientX-e.target.offsetLeft) * e.target.videoWidth)/e.target.clientWidth
      let y = ((e.clientY-e.target.offsetTop) * e.target.videoHeight)/e.target.clientHeight
      console.log('video click', x, y)
      await fetch('/debug/click/'+x+'/'+y)
    }
    async function videoKeyPress(e) {
      console.log('video keypress', e.key)
      await fetch('/debug/keypress/'+e.key)
    }
    document.addEventListener('keypress', videoKeyPress)
    </script>
    <video style="width: 100%; height: 100%" onKeyPress="videoKeyPress(event)" onClick="videoClick(event)" src="/stream?waitForVideo=false&url=${encodeURIComponent(
      req.query.url || 'https://google.com'
    )}" autoplay muted />
    </html>`)
  })

  app.get('/debug/click/:x/:y', async (req, res) => {
    let browser = await getCurrentBrowser()
    let pages = await browser.pages()
    if (pages.length == 0) {
      res.send('false')
      return
    }
    let page = pages[pages.length - 1]
    await page.mouse.click(parseInt(req.params.x), parseInt(req.params.y))
    res.send('true')
  })

  app.get('/debug/keypress/:key', async (req, res) => {
    let browser = await getCurrentBrowser()
    let pages = await browser.pages()
    if (pages.length == 0) {
      res.send('false')
      return
    }
    let page = pages[pages.length - 1]
    await page.keyboard.press(req.params.key)
    res.send('true')
  })

  app.get('/stream/:name?', async (req, res) => {
    var u = req.query.url
    let name = req.params.name
    if (name) {
      u = {
        nbc: 'https://www.nbc.com/live?brand=nbc&callsign=nbc',
        cnbc: 'https://www.nbc.com/live?brand=cnbc&callsign=cnbc',
        msnbc: 'https://www.nbc.com/live?brand=msnbc&callsign=msnbc',
        nbcnews: 'https://www.nbc.com/live?brand=nbc-news&callsign=nbcnews',
        bravo: 'https://www.nbc.com/live?brand=bravo&callsign=bravo_east',
        bravop: 'https://www.nbc.com/live?brand=bravo&callsign=bravo_west',
        e: 'https://www.nbc.com/live?brand=e&callsign=e_east',
        ep: 'https://www.nbc.com/live?brand=e&callsign=e_west',
        golf: 'https://www.nbc.com/live?brand=golf&callsign=golf',
        oxygen: 'https://www.nbc.com/live?brand=oxygen&callsign=oxygen_east',
        oxygenp: 'https://www.nbc.com/live?brand=oxygen&callsign=oxygen_west',
        syfy: 'https://www.nbc.com/live?brand=syfy&callsign=syfy_east',
        syfyp: 'https://www.nbc.com/live?brand=syfy&callsign=syfy_west',
        usa: 'https://www.nbc.com/live?brand=usa&callsign=usa_east',
        usap: 'https://www.nbc.com/live?brand=usa&callsign=usa_west',
        universo: 'https://www.nbc.com/live?brand=nbc-universo&callsign=universo_east',
        universop: 'https://www.nbc.com/live?brand=nbc-universo&callsign=universo_west',
        necn: 'https://www.nbc.com/live?brand=necn&callsign=necn',
        nbcsbayarea: 'https://www.nbc.com/live?brand=rsn-bay-area&callsign=nbcsbayarea',
        nbcsboston: 'https://www.nbc.com/live?brand=rsn-boston&callsign=nbcsboston',
        nbcscalifornia: 'https://www.nbc.com/live?brand=rsn-california&callsign=nbcscalifornia',
        nbcschicago: 'https://www.nbc.com/live?brand=rsn-chicago&callsign=nbcschicago',
        nbcsphiladelphia: 'https://www.nbc.com/live?brand=rsn-philadelphia&callsign=nbcsphiladelphia',
        nbcswashington: 'https://www.nbc.com/live?brand=rsn-washington&callsign=nbcswashington',
        weatherscan: 'https://weatherscan.net/',
        windy: 'https://windy.com',
      }[name]
    }

    var waitForVideo = req.query.waitForVideo === 'false' ? false : true
    switch (name) {
      case 'weatherscan':
      case 'windy':
        waitForVideo = false
    }
    var minimizeWindow = false
    if (process.platform == 'darwin' && waitForVideo) minimizeWindow = true

    var browser, page
    try {
      browser = await getCurrentBrowser()
      page = await browser.newPage()
      //await page.setBypassCSP(true)
      //page.on('console', msg => console.log(msg.text()))
    } catch (e) {
      console.log('failed to start browser page', u, e)
      res.status(500).send(`failed to start browser page: ${e}`)
      return
    }

    try {
      const stream = await getStream(page, {
        video: true,
        audio: true,
        videoBitsPerSecond: 8000000,
        audioBitsPerSecond: 192000,
        mimeType: 'video/webm;codecs=H264',
        videoConstraints: {
          mandatory: {
            minWidth: viewport.width,
            minHeight: viewport.height,
            maxWidth: viewport.width,
            maxHeight: viewport.height,
            minFrameRate: 60,
          },
        },
      })

      console.log('streaming', u)
      stream.pipe(res)
      res.on('close', async err => {
        await stream.destroy()
        await page.close()
        console.log('finished', u)
      })
    } catch (e) {
      console.log('failed to start stream', u, e)
      res.status(500).send(`failed to start stream: ${e}`)
      await page.close()
      return
    }

    try {
      await page.goto(u)
      if (waitForVideo) {
        await page.waitForSelector('video')
        await page.waitForFunction(`(function() {
          let video = document.querySelector('video')
          return video.readyState === 4
        })()`)
        await page.evaluate(`(function() {
          let video = document.querySelector('video')
          video.style.setProperty('position', 'fixed', 'important')
          video.style.top = '0'
          video.style.left = '0'
          video.style.width = '100%'
          video.style.height = '100%'
          video.style.zIndex = '999000'
          video.style.background = 'black'
          video.style.cursor = 'none'
          video.style.transform = 'translate(0, 0)'
          video.style.objectFit = 'contain'
          video.play()
          video.muted = false
          video.removeAttribute('muted')

          let header = document.querySelector('.header-container')
          if (header) {
            header.style.zIndex = '0'
          }
        })()`)
      }
    if (page.url().includes('peacocktv.com')) {
        await page.waitForSelector('.playback-button-icon.volume-muted', { visible: true }); // Wait for the element to be visible

        await page.evaluate(() => {
            const svgElement = document.querySelector('.playback-button-icon.volume-muted');
            if (svgElement) {
                svgElement.dispatchEvent(new MouseEvent('click', { bubbles: true })); // Dispatch a click event on the SVG element
            } else {
                console.error('SVG element not found');
            }
        });
    }
      const uiSize = await page.evaluate(`(function() {
        return {
          height: window.outerHeight - window.innerHeight,
          width: window.outerWidth - window.innerWidth,
        }
      })()`)
      const session = await page.target().createCDPSession()
      const {windowId} = await session.send('Browser.getWindowForTarget')
      await session.send('Browser.setWindowBounds', {
        windowId,
        bounds: {
          height: viewport.height + uiSize.height,
          width: viewport.width + uiSize.width,
        },
      })
      if (minimizeWindow) {
        await session.send('Browser.setWindowBounds', {
          windowId,
          bounds: {
            windowState: 'minimized',
          },
        })
      }
    } catch (e) {
      console.log('failed to stream', u, e)
    }
  })

  const server = app.listen(5589, () => {
    console.log('Chrome Capture server listening on port 5589')
  })
}

main()

I am getting an error:

Blockquote
failed to deploy a stack: cc4c Pulling cc4c Error Error response from daemon: manifest for bnhf/cc4c:latest not found: manifest unknown: manifest unknown

TAG=test

I'm a moron - I JUST came here to say that LOL... deploying now. Did my post with the main.js and the peacock code help?

I don't use cc4c, but what I meant was you should set the Environment variable to
HOST_VOLUME=/volume1/docker/cc4c
That way the main.js file will be easier to access there /volume1/docker/cc4c/main.js

1 Like

Yes. Working on it now.

@bnhf Thank you!! - FYI, I tried launching one of the Peacock channels and it just loads a black screen... Weatherscan is still working OK.

Good idea, but in this case we need the contents of the directory copied from the container -- which is the default with Docker Volumes. When you do a directory binding, it starts empty and you need to specifically copy the contents over. Not difficult to do, but I've been trying to keep the compose and container compatible with the original fancybits image.

2 Likes

I restarted the new test stack and Peacock still generates the "Something went wrong." screen with dialog that the system configuration is not compatible. I guess they can see what type of system is trying to access the stream, even though it's Chrome 127 now... hrm. They're annoying LOL.

@bnhf Something else I noticed, I uncommented the intel QSV settings since I would like to use my GPU, but now Chrome 127 isn't reporting that hardware is being used. Just software acceleration (chrome://gpu). Previously, that was working. Not sure what might have changed, but wanted to let you know.

You may need to play around with enabling some of these args. The user that's reporting the best results with the container version of cc4c has them all enabled -- but he's using the original container. As long as you delete the cc4c_config volume between, you should be able to switch back-and-forth between the bnhf/cc4c image and the original fancybits image.

I was using all those with the fancy-bits container and the GPU hardware acceleration was working as expected with all those args. Switching to your container is when it stopped... maybe there's something in main.js that's changed or it's an issue with Chrome 127? Am not sure on that one... it was working before and after I made edits to main.js earlier before trying your test container.

I’m getting a different error:

Failure

failed to deploy a stack: cc4c

Pulling no matching manifest for linux/arm64/v8 in the manifest list entries

The cc4c Docker container is amd64 only, due to the lack of a Linux chromium-browser for arm64.

Ah, didn’t realize that. I guess that means I need to resurrect my original cc4c setup. Thanks for working me through this.

I was thinking about this is a bit more. This did run initially, just in a very small screen, with black bars all around. Was that using some sort of emulation mode? It wasn't until making some of the changes that I got the error message (failed to deploy a stack: cc4c).

I forgot that you'd had some level of success running the original fancybits image. You're running this on Mac Mini with an Apple chip right? I don't think you should be able to install either of these:

I’m thinking back (because I’ve since deleted the stack), but I know I installed it via the button in Project One Click, so whatever that installs is what I had.

I am struggling connecting to the docker using a vnc client. I can have the dvr connect and it opens a stream, but I’m not logged in. When I try to connect with a vnc client it can’t connect. I’ve tried running the docker on windows and a NAS, neither have connectable vnc servers. Any ideas on what I’m missed? Thanks everyone.

1 Like

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.