Search code examples
javascriptpromisebluebird

Using Promises to defer continuation within a forEach loop


My goal with the below is to:

  • Capture a list of resolutions
  • Scan through each of them (in order) to find the first one that results in a successful stream

To test this, I have testVideoPresence:

var testCounter = 0;
function testVideoPresence(videoElement) {
    testCounter++;
    if (testCounter >= 5) {
        testCounter = 0;
        return false;
    }
    if (!videoElement.videoWidth || videoElement.videoWidth < 10) { // check to prevent 2x2 issue
        setTimeout(function() {
            testVideoPresence(videoElement); // try again
        }, 500);
    } else if (video.videoWidth * video.videoHeight > 0) {
        return true;
    }
}

As you can see, I'm using a setTimeout to recurse at most 5 times. This is where things get tricky:

 resolutionTestBuilder.buildTests().then(function (resolutionTests) {
        // at this point, I have a set of resolutions that I want to try
        resolutionTests.forEach(function (resolutionTest) {
            // then I want to iterate over all of them until I find one that works
            performTest(resolutionTest).then(function (result) {
                video.srcObject = result.mediaStream; // start streaming to dom
                if (testVideoPresence(video)) { // here is the pain point - how do I await the result of this within the forEach?
                    // return the dimensions
                } else {
                    // continue scanning
                }
            }).catch(function (error) {
                logger.internalLog(error);
            });

            // wait to continue until we have our result

        });
    }).catch(function (error) {
        logger.internalLog(error);
    });

function performTest(currentTest) {
        return streamHelper.openStream(currentTest.device, currentTest.resolution).then(function(streamData) {
            return streamData;
        }).catch(function (error) {
            logger.internalLog(error);
        });;
    };

streamHelper.openStream = function (device, resolution) {
    var constraints = createVideoConstraints(device, resolution);
    logger.internalLog("openStream:" + resolution.label + ": " + resolution.width + "x" + resolution.height);
    return navigator.mediaDevices.getUserMedia(constraints)
        .then(function (mediaStream) {
            streamHelper.activeStream = mediaStream;
            return { stream: mediaStream, resolution: resolution, constraints: constraints };
            // video.srcObject = mediaStream; // push mediaStream into target element.  This triggers doScan.
        })
        .catch(function (error) {
            if (error.name == "NotAllowedError") {
                return error.name;
            } else {
                return error;
            }
        });
 };

I'm trying to wait for the result within the forEach before continuing through the array of resolutions. I know I can use some advanced techniques like async/await if I want to transpile - but I'm stuck with vanilla JS and promises / bluebird.js for now. What are my options? Disclaimer - I am new to promises so the above code could be very malformed.

Update:

Tests are defined in order of importance - so I do need resolutionTests[0] to resolve before resolutionTests[1].


Solution

  • If the order of trials isn't important, you can simply use a map combined with Promise.race to make sure the first promise of a list that resolves resolves the whole list. You also need to make sure your promises return other promises inside then.

    resolutionTestBuilder.buildTests().then(function (resolutionTests) {
        return Promise.race(resolutionTests.map(function (resolutionTest) {
            return performTest(resolutionTest).then(function (result) {
                video.srcObject = result.mediaStream; // start streaming to dom
                return testVideoPresence(video);
            }).catch(function (error) {
                logger.internalLog(error);
            });
        }));
    }).catch(function (error) {
        logger.internalLog(error);
    });
    

    This of course assumes that testVideoPresence does NOT resolve when you the dimensions are not available.

    If the order of trial is important then a reduce approach might work.

    This will basically result in a sequential application of the promises and the resulting promise will until all of them are resolved.

    However, once the solution is found we attach it to the collector of the reduce so that further trials simply return that as well and avoid further tests (because by the time this is found the chain is already registered)

    return resolutionTests.reduce(function(result, resolutionTest) {
        var nextPromise = result.intermPromise.then(function() {
            if (result.found) { // result will contain found whenver the first promise that resolves finds this
                return Promise.resolve(result.found);   // this simply makes sure that the promises registered after a result found will return it as well
            } else {
                return performTest(resolutionTest).then(function (result) {
                    video.srcObject = result.mediaStream; // start streaming to dom
                    return testVideoPresence(video).then(function(something) {
                        result.found = something;
                        return result.found;
                    });
                }).catch(function (error) {
                    logger.internalLog(error);
                });
            }
        );
        return { intermPromise: nextPromise, found: result.found };
    }, { intermPromise: Promise.resolve() }); // start reduce with a result with no 'found' and a unit Promise