diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js
new file mode 100644
index 00000000000..79e378a880d
--- /dev/null
+++ b/modules/relaidoBidAdapter.js
@@ -0,0 +1,296 @@
+import * as utils from '../src/utils.js';
+import { registerBidder } from '../src/adapters/bidderFactory.js';
+import { BANNER, VIDEO } from '../src/mediaTypes.js';
+import { Renderer } from '../src/Renderer.js';
+import { getStorageManager } from '../src/storageManager.js';
+
+const BIDDER_CODE = 'relaido';
+const BIDDER_DOMAIN = 'api.relaido.jp';
+const ADAPTER_VERSION = '1.0.0';
+const DEFAULT_TTL = 300;
+const UUID_KEY = 'relaido_uuid';
+
+const storage = getStorageManager();
+
+function isBidRequestValid(bid) {
+ if (!utils.isSafariBrowser() && !hasUuid()) {
+ utils.logWarn('uuid is not found.');
+ return false;
+ }
+ if (!utils.deepAccess(bid, 'params.placementId')) {
+ utils.logWarn('placementId param is reqeuired.');
+ return false;
+ }
+ if (hasVideoMediaType(bid)) {
+ if (!isVideoValid(bid)) {
+ utils.logWarn('Invalid mediaType video.');
+ return false;
+ }
+ } else if (hasBannerMediaType(bid)) {
+ if (!isBannerValid(bid)) {
+ utils.logWarn('Invalid mediaType banner.');
+ return false;
+ }
+ } else {
+ utils.logWarn('Invalid mediaTypes input banner or video.');
+ return false;
+ }
+ return true;
+}
+
+function buildRequests(validBidRequests, bidderRequest) {
+ let bidRequests = [];
+
+ for (let i = 0; i < validBidRequests.length; i++) {
+ const bidRequest = validBidRequests[i];
+ const placementId = utils.getBidIdParameter('placementId', bidRequest.params);
+ const bidDomain = bidRequest.params.domain || BIDDER_DOMAIN;
+ const bidUrl = `https://${bidDomain}/vast/v1/out/bid/${placementId}`;
+ const uuid = getUuid();
+ const mediaType = getMediaType(bidRequest);
+
+ let payload = {
+ version: ADAPTER_VERSION,
+ ref: bidderRequest.refererInfo.referer,
+ timeout_ms: bidderRequest.timeout,
+ ad_unit_code: bidRequest.adUnitCode,
+ auction_id: bidRequest.auctionId,
+ bidder: bidRequest.bidder,
+ bidder_request_id: bidRequest.bidderRequestId,
+ bid_requests_count: bidRequest.bidRequestsCount,
+ bid_id: bidRequest.bidId,
+ transaction_id: bidRequest.transactionId,
+ media_type: mediaType,
+ uuid: uuid,
+ };
+
+ if (hasVideoMediaType(bidRequest)) {
+ const playerSize = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize');
+ payload.width = playerSize[0][0];
+ payload.height = playerSize[0][1];
+ } else if (hasBannerMediaType(bidRequest)) {
+ const sizes = utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes');
+ payload.width = sizes[0][0];
+ payload.height = sizes[0][1];
+ }
+
+ bidRequests.push({
+ method: 'GET',
+ url: bidUrl,
+ data: payload,
+ options: {
+ withCredentials: true
+ },
+ bidId: bidRequest.bidId,
+ player: bidRequest.params.player,
+ width: payload.width,
+ height: payload.height,
+ mediaType: mediaType,
+ });
+ }
+
+ return bidRequests;
+}
+
+function interpretResponse(serverResponse, bidRequest) {
+ const bidResponses = [];
+ const body = serverResponse.body;
+ if (!body || body.status != 'ok') {
+ return [];
+ }
+
+ if (body.uuid) {
+ storage.setDataInLocalStorage(UUID_KEY, body.uuid);
+ }
+
+ const playerUrl = bidRequest.player || body.playerUrl;
+ const mediaType = bidRequest.mediaType || VIDEO;
+
+ let bidResponse = {
+ requestId: bidRequest.bidId,
+ width: bidRequest.width,
+ height: bidRequest.height,
+ cpm: body.price,
+ currency: body.currency,
+ creativeId: body.creativeId,
+ dealId: body.dealId || '',
+ ttl: body.ttl || DEFAULT_TTL,
+ netRevenue: true,
+ mediaType: mediaType,
+ };
+ if (mediaType === VIDEO) {
+ bidResponse.vastXml = body.vast;
+ bidResponse.renderer = newRenderer(bidRequest.bidId, playerUrl);
+ } else {
+ const playerTag = createPlayerTag(playerUrl);
+ const renderTag = createRenderTag(bidRequest.width, bidRequest.height, body.vast);
+ bidResponse.ad = `
${playerTag}${renderTag}
`;
+ }
+ bidResponses.push(bidResponse);
+
+ return bidResponses;
+}
+
+function getUserSyncs(syncOptions, serverResponses) {
+ const resSyncUrl = utils.deepAccess(serverResponses, '0.body.syncUrl');
+ const proSyncUrl = `https://${BIDDER_DOMAIN}/tr/v1/prebid/sync.html`;
+ const syncUrl = resSyncUrl || proSyncUrl;
+ receiveMessage();
+ return [{
+ type: 'iframe',
+ url: syncUrl
+ }];
+}
+
+function onBidWon(bid) {
+ let query = utils.parseQueryStringParameters({
+ placement_id: utils.deepAccess(bid, 'params.0.placementId'),
+ creative_id: utils.deepAccess(bid, 'creativeId'),
+ price: utils.deepAccess(bid, 'cpm'),
+ auction_id: utils.deepAccess(bid, 'auctionId'),
+ bid_id: utils.deepAccess(bid, 'requestId'),
+ ad_id: utils.deepAccess(bid, 'adId'),
+ ad_unit_code: utils.deepAccess(bid, 'adUnitCode'),
+ ref: window.location.href,
+ }).replace(/\&$/, '');
+ const bidDomain = utils.deepAccess(bid, 'params.0.domain') || BIDDER_DOMAIN;
+ const burl = `https://${bidDomain}/tr/v1/prebid/win.gif?${query}`;
+ utils.triggerPixel(burl);
+}
+
+function onTimeout(data) {
+ let query = utils.parseQueryStringParameters({
+ placement_id: utils.deepAccess(data, '0.params.0.placementId'),
+ timeout: utils.deepAccess(data, '0.timeout'),
+ auction_id: utils.deepAccess(data, '0.auctionId'),
+ bid_id: utils.deepAccess(data, '0.bidId'),
+ ad_unit_code: utils.deepAccess(data, '0.adUnitCode'),
+ ref: window.location.href,
+ }).replace(/\&$/, '');
+ const bidDomain = utils.deepAccess(data, '0.params.0.domain') || BIDDER_DOMAIN;
+ const timeoutUrl = `https://${bidDomain}/tr/v1/prebid/timeout.gif?${query}`;
+ utils.triggerPixel(timeoutUrl);
+}
+
+function createPlayerTag(playerUrl) {
+ return ``;
+}
+
+function createRenderTag(width, height, vast) {
+ return ``;
+};
+
+function newRenderer(bidId, playerUrl) {
+ const renderer = Renderer.install({
+ id: bidId,
+ url: playerUrl,
+ loaded: false
+ });
+ try {
+ renderer.setRender(outstreamRender);
+ } catch (err) {
+ utils.logWarn('renderer.setRender Error', err);
+ }
+ return renderer;
+}
+
+function outstreamRender(bid) {
+ bid.renderer.push(() => {
+ window.RelaidoPlayer.renderAd({
+ adUnitCode: bid.adUnitCode,
+ width: bid.width,
+ height: bid.height,
+ vastXml: bid.vastXml,
+ mediaType: bid.mediaType,
+ });
+ });
+}
+
+function receiveMessage() {
+ window.addEventListener('message', function (e) {
+ if (e.data && e.data.relaido_uuid) {
+ storage.setDataInLocalStorage(UUID_KEY, e.data.relaido_uuid);
+ }
+ });
+}
+
+function isBannerValid(bid) {
+ if (!isMobile()) {
+ return false;
+ }
+ const sizes = utils.deepAccess(bid, 'mediaTypes.banner.sizes');
+ if (sizes && utils.isArray(sizes)) {
+ if (utils.isArray(sizes[0])) {
+ const width = sizes[0][0];
+ const height = sizes[0][1];
+ if (width >= 300 && height >= 250) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+function isVideoValid(bid) {
+ const playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize');
+ if (playerSize && utils.isArray(playerSize) && playerSize.length > 0) {
+ const context = utils.deepAccess(bid, 'mediaTypes.video.context');
+ if (context && context === 'outstream') {
+ return true;
+ }
+ }
+ return false;
+}
+
+function hasUuid() {
+ return !!storage.getDataFromLocalStorage(UUID_KEY);
+}
+
+function getUuid() {
+ return storage.getDataFromLocalStorage(UUID_KEY) || '';
+}
+
+export function isMobile() {
+ const ua = navigator.userAgent;
+ if (ua.indexOf('iPhone') > -1 || ua.indexOf('iPod') > -1 || (ua.indexOf('Android') > -1 && ua.indexOf('Tablet') == -1)) {
+ return true;
+ }
+ return false;
+}
+
+function getMediaType(bid) {
+ if (hasVideoMediaType(bid)) {
+ return VIDEO;
+ } else if (hasBannerMediaType(bid)) {
+ return BANNER;
+ }
+ return '';
+}
+
+function hasBannerMediaType(bid) {
+ return !!utils.deepAccess(bid, 'mediaTypes.banner');
+}
+
+function hasVideoMediaType(bid) {
+ return !!utils.deepAccess(bid, 'mediaTypes.video');
+}
+
+export const spec = {
+ code: BIDDER_CODE,
+ supportedMediaTypes: [BANNER, VIDEO],
+ isBidRequestValid,
+ buildRequests,
+ interpretResponse,
+ getUserSyncs: getUserSyncs,
+ onBidWon,
+ onTimeout
+}
+
+registerBidder(spec);
diff --git a/modules/relaidoBidAdapter.md b/modules/relaidoBidAdapter.md
new file mode 100644
index 00000000000..459f772c66b
--- /dev/null
+++ b/modules/relaidoBidAdapter.md
@@ -0,0 +1,48 @@
+# Overview
+
+```
+Module Name: Relaido Bidder Adapter
+Module Type: Bidder Adapter
+Maintainer: video-dev@cg.relaido.co.jp
+```
+
+# Description
+
+Connects to Relaido exchange for bids.
+
+Relaido bid adapter supports Outstream Video.
+
+# Test Parameters
+
+```javascript
+ var adUnits=[{
+ code: 'banner-ad-div',
+ mediaTypes: {
+ banner: {
+ sizes: [
+ [300, 250]
+ ]
+ }
+ },
+ bids: [{
+ bidder: 'relaido',
+ params: {
+ placementId: '9900'
+ }
+ }]
+ },{
+ code: 'video-ad-player',
+ mediaTypes: {
+ video: {
+ context: 'outstream',
+ playerSize: [640, 360]
+ }
+ },
+ bids: [{
+ bidder: 'relaido',
+ params: {
+ placementId: '9900'
+ }
+ }]
+ }];
+```
\ No newline at end of file
diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js
new file mode 100644
index 00000000000..2390f62f875
--- /dev/null
+++ b/test/spec/modules/relaidoBidAdapter_spec.js
@@ -0,0 +1,311 @@
+import { expect } from 'chai';
+import { spec } from 'modules/relaidoBidAdapter.js';
+import * as url from 'src/url.js';
+import * as utils from 'src/utils.js';
+
+const UUID_KEY = 'relaido_uuid';
+const DEFAULT_USER_AGENT = window.navigator.userAgent;
+const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1';
+
+const setUADefault = () => { window.navigator.__defineGetter__('userAgent', function () { return DEFAULT_USER_AGENT }) };
+const setUAMobile = () => { window.navigator.__defineGetter__('userAgent', function () { return MOBILE_USER_AGENT }) };
+
+describe('RelaidoAdapter', function () {
+ const relaido_uuid = 'hogehoge';
+ let bidRequest;
+ let bidderRequest;
+ let serverResponse;
+ let serverRequest;
+
+ beforeEach(function () {
+ bidRequest = {
+ bidder: 'relaido',
+ params: {
+ placementId: '100000',
+ },
+ mediaTypes: {
+ video: {
+ context: 'outstream',
+ playerSize: [
+ [640, 360]
+ ]
+ }
+ },
+ adUnitCode: 'test',
+ bidId: '2ed93003f7bb99',
+ bidderRequestId: '1c50443387a1f2',
+ auctionId: '413ed000-8c7a-4ba1-a1fa-9732e006f8c3',
+ transactionId: '5c2d064c-7b76-42e8-a383-983603afdc45',
+ bidRequestsCount: 1,
+ bidderRequestsCount: 1,
+ bidderWinsCount: 0
+ };
+ bidderRequest = {
+ timeout: 1000,
+ refererInfo: {
+ referer: 'https://publisher.com/home'
+ }
+ };
+ serverResponse = {
+ body: {
+ status: 'ok',
+ price: 500,
+ model: 'vcpm',
+ currency: 'JPY',
+ creativeId: 1000,
+ uuid: relaido_uuid,
+ vast: '',
+ playerUrl: 'https://relaido/player.js',
+ syncUrl: 'https://relaido/sync.html'
+ }
+ };
+ serverRequest = {
+ method: 'GET',
+ bidId: bidRequest.bidId,
+ width: bidRequest.mediaTypes.video.playerSize[0][0],
+ height: bidRequest.mediaTypes.video.playerSize[0][1],
+ mediaType: 'video',
+ };
+ localStorage.setItem(UUID_KEY, relaido_uuid);
+ });
+
+ describe('spec.isBidRequestValid', function () {
+ it('should return true when the required params are passed by video', function () {
+ expect(spec.isBidRequestValid(bidRequest)).to.equal(true);
+ });
+
+ it('should return true when the required params are passed by banner', function () {
+ setUAMobile();
+ bidRequest.mediaTypes = {
+ banner: {
+ sizes: [
+ [300, 250]
+ ]
+ }
+ };
+ expect(spec.isBidRequestValid(bidRequest)).to.equal(true);
+ setUADefault();
+ });
+
+ it('should return false when the uuid are missing', function () {
+ localStorage.removeItem(UUID_KEY);
+ const result = !!(utils.isSafariBrowser());
+ expect(spec.isBidRequestValid(bidRequest)).to.equal(result);
+ });
+
+ it('should return false when the placementId params are missing', function () {
+ bidRequest.params.placementId = undefined;
+ expect(spec.isBidRequestValid(bidRequest)).to.equal(false);
+ });
+
+ it('should return false when the mediaType video params are missing', function () {
+ bidRequest.mediaTypes = {
+ video: {}
+ };
+ expect(spec.isBidRequestValid(bidRequest)).to.equal(false);
+ });
+
+ it('should return false when the mediaType banner params are missing', function () {
+ setUAMobile();
+ bidRequest.mediaTypes = {
+ banner: {}
+ };
+ expect(spec.isBidRequestValid(bidRequest)).to.equal(false);
+ setUADefault();
+ });
+
+ it('should return false when the non-mobile', function () {
+ bidRequest.mediaTypes = {
+ banner: {
+ sizes: [
+ [300, 250]
+ ]
+ }
+ };
+ expect(spec.isBidRequestValid(bidRequest)).to.equal(false);
+ });
+
+ it('should return false when the mediaTypes params are missing', function () {
+ bidRequest.mediaTypes = {};
+ expect(spec.isBidRequestValid(bidRequest)).to.equal(false);
+ });
+ });
+
+ describe('spec.buildRequests', function () {
+ it('should build bid requests by video', function () {
+ const bidRequests = spec.buildRequests([bidRequest], bidderRequest);
+ expect(bidRequests).to.have.lengthOf(1);
+ const request = bidRequests[0];
+ expect(request.method).to.equal('GET');
+ expect(request.url).to.equal('https://api.relaido.jp/vast/v1/out/bid/100000');
+ expect(request.bidId).to.equal(bidRequest.bidId);
+ expect(request.width).to.equal(bidRequest.mediaTypes.video.playerSize[0][0]);
+ expect(request.height).to.equal(bidRequest.mediaTypes.video.playerSize[0][1]);
+ expect(request.mediaType).to.equal('video');
+ expect(request.data.ref).to.equal(bidderRequest.refererInfo.referer);
+ expect(request.data.timeout_ms).to.equal(bidderRequest.timeout);
+ expect(request.data.ad_unit_code).to.equal(bidRequest.adUnitCode);
+ expect(request.data.auction_id).to.equal(bidRequest.auctionId);
+ expect(request.data.bidder).to.equal(bidRequest.bidder);
+ expect(request.data.bidder_request_id).to.equal(bidRequest.bidderRequestId);
+ expect(request.data.bid_requests_count).to.equal(bidRequest.bidRequestsCount);
+ expect(request.data.bid_id).to.equal(bidRequest.bidId);
+ expect(request.data.transaction_id).to.equal(bidRequest.transactionId);
+ expect(request.data.media_type).to.equal('video');
+ expect(request.data.uuid).to.equal(relaido_uuid);
+ expect(request.data.width).to.equal(bidRequest.mediaTypes.video.playerSize[0][0]);
+ expect(request.data.height).to.equal(bidRequest.mediaTypes.video.playerSize[0][1]);
+ });
+
+ it('should build bid requests by banner', function () {
+ bidRequest.mediaTypes = {
+ banner: {
+ sizes: [
+ [640, 360]
+ ]
+ }
+ };
+ const bidRequests = spec.buildRequests([bidRequest], bidderRequest);
+ expect(bidRequests).to.have.lengthOf(1);
+ const request = bidRequests[0];
+ expect(request.mediaType).to.equal('banner');
+ });
+ });
+
+ describe('spec.interpretResponse', function () {
+ it('should build bid response by video', function () {
+ const bidResponses = spec.interpretResponse(serverResponse, serverRequest);
+ expect(bidResponses).to.have.lengthOf(1);
+ const response = bidResponses[0];
+ expect(response.requestId).to.equal(serverRequest.bidId);
+ expect(response.width).to.equal(serverRequest.width);
+ expect(response.height).to.equal(serverRequest.height);
+ expect(response.cpm).to.equal(serverResponse.body.price);
+ expect(response.currency).to.equal(serverResponse.body.currency);
+ expect(response.creativeId).to.equal(serverResponse.body.creativeId);
+ expect(response.vastXml).to.equal(serverResponse.body.vast);
+ expect(response.ad).to.be.undefined;
+ });
+
+ it('should build bid response by banner', function () {
+ serverRequest.mediaType = 'banner';
+ const bidResponses = spec.interpretResponse(serverResponse, serverRequest);
+ expect(bidResponses).to.have.lengthOf(1);
+ const response = bidResponses[0];
+ expect(response.requestId).to.equal(serverRequest.bidId);
+ expect(response.width).to.equal(serverRequest.width);
+ expect(response.height).to.equal(serverRequest.height);
+ expect(response.cpm).to.equal(serverResponse.body.price);
+ expect(response.currency).to.equal(serverResponse.body.currency);
+ expect(response.creativeId).to.equal(serverResponse.body.creativeId);
+ expect(response.vastXml).to.be.undefined;
+ expect(response.ad).to.include(``);
+ expect(response.ad).to.include(``);
+ expect(response.ad).to.include(`window.RelaidoPlayer.renderAd`);
+ });
+
+ it('should not build bid response', function () {
+ serverResponse = {};
+ const bidResponses = spec.interpretResponse(serverResponse, serverRequest);
+ expect(bidResponses).to.have.lengthOf(0);
+ });
+
+ it('should not build bid response', function () {
+ serverResponse = {
+ body: {
+ status: 'no_ad',
+ }
+ };
+ const bidResponses = spec.interpretResponse(serverResponse, serverRequest);
+ expect(bidResponses).to.have.lengthOf(0);
+ });
+ });
+
+ describe('spec.getUserSyncs', function () {
+ it('should choose iframe sync urls', function () {
+ let userSyncs = spec.getUserSyncs({}, [serverResponse]);
+ expect(userSyncs).to.deep.equal([{
+ type: 'iframe',
+ url: serverResponse.body.syncUrl
+ }]);
+ });
+
+ it('should choose iframe sync urls if syncUrl are undefined', function () {
+ serverResponse.body.syncUrl = undefined;
+ let userSyncs = spec.getUserSyncs({}, [serverResponse]);
+ expect(userSyncs).to.deep.equal([{
+ type: 'iframe',
+ url: 'https://api.relaido.jp/tr/v1/prebid/sync.html'
+ }]);
+ });
+ });
+
+ describe('spec.onBidWon', function () {
+ let stub;
+ beforeEach(() => {
+ stub = sinon.stub(utils, 'triggerPixel');
+ });
+ afterEach(() => {
+ stub.restore();
+ });
+
+ it('Should create nurl pixel if bid nurl', function () {
+ let bid = {
+ bidder: bidRequest.bidder,
+ creativeId: serverResponse.body.creativeId,
+ cpm: serverResponse.body.price,
+ params: [bidRequest.params],
+ auctionId: bidRequest.auctionId,
+ requestId: bidRequest.bidId,
+ adId: '3b286a4db7031f',
+ adUnitCode: bidRequest.adUnitCode,
+ ref: window.location.href,
+ }
+ spec.onBidWon(bid);
+ const parser = url.parse(stub.getCall(0).args[0]);
+ const query = parser.search;
+ expect(parser.hostname).to.equal('api.relaido.jp');
+ expect(parser.pathname).to.equal('/tr/v1/prebid/win.gif');
+ expect(query.placement_id).to.equal('100000');
+ expect(query.creative_id).to.equal('1000');
+ expect(query.price).to.equal('500');
+ expect(query.auction_id).to.equal('413ed000-8c7a-4ba1-a1fa-9732e006f8c3');
+ expect(query.bid_id).to.equal('2ed93003f7bb99');
+ expect(query.ad_id).to.equal('3b286a4db7031f');
+ expect(query.ad_unit_code).to.equal('test');
+ expect(query.ref).to.include(window.location.href);
+ });
+ });
+
+ describe('spec.onTimeout', function () {
+ let stub;
+ beforeEach(() => {
+ stub = sinon.stub(utils, 'triggerPixel');
+ });
+ afterEach(() => {
+ stub.restore();
+ });
+
+ it('Should create nurl pixel if bid nurl', function () {
+ const data = [{
+ bidder: bidRequest.bidder,
+ bidId: bidRequest.bidId,
+ adUnitCode: bidRequest.adUnitCode,
+ auctionId: bidRequest.auctionId,
+ params: [bidRequest.params],
+ timeout: bidderRequest.timeout,
+ }];
+ spec.onTimeout(data);
+ const parser = url.parse(stub.getCall(0).args[0]);
+ const query = parser.search;
+ expect(parser.hostname).to.equal('api.relaido.jp');
+ expect(parser.pathname).to.equal('/tr/v1/prebid/timeout.gif');
+ expect(query.placement_id).to.equal('100000');
+ expect(query.timeout).to.equal('1000');
+ expect(query.auction_id).to.equal('413ed000-8c7a-4ba1-a1fa-9732e006f8c3');
+ expect(query.bid_id).to.equal('2ed93003f7bb99');
+ expect(query.ad_unit_code).to.equal('test');
+ expect(query.ref).to.include(window.location.href);
+ });
+ });
+});